Compare commits

..

12 Commits

Author SHA1 Message Date
e3378a257e v0.17.0 2021-11-04 09:28:18 +08:00
75302195e3 backup: plugins
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-11-04 09:08:31 +08:00
78ab1a73d6 feat: 优化WebSocket客户端 兼容1.7.10
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-11-04 09:07:48 +08:00
867fc802ec feat: 优化websocket
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-10-25 15:56:23 +08:00
ed588e4502 feat: 新增公告接口
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-10-25 15:54:38 +08:00
cbf00d107e feat: 新增充值前后玩家在线状态检测
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-23 10:41:47 +08:00
b9a9334655 feat: 插件备份
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-14 12:45:51 +08:00
2a58ad46d2 chore: 依赖版本更新
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-14 12:45:38 +08:00
b301948583 feat: 配置文件调试日志限制500字符
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-14 12:44:58 +08:00
b21aa1051d feat: 完善client相关功能 重构server部分
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-14 12:43:20 +08:00
0f418f39df v0.16.3 2021-08-05 14:39:53 +08:00
75cb430230 feat: 优化WebSocket客户端 支持WSS
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-05 14:35:17 +08:00
78 changed files with 3618 additions and 269 deletions

View File

@@ -1,5 +1,5 @@
{
"version": "0.16.2",
"version": "0.17.0",
"useWorkspaces": true,
"npmClient": "yarn",
"packages": [

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/amqp",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript amqp package",
"keywords": [
"miaoscript",
@@ -19,12 +19,12 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
"@ccms/api": "^0.17.0",
"@ccms/common": "^0.17.0",
"@ccms/container": "^0.17.0"
},
"devDependencies": {
"@ccms/nashorn": "^0.16.0",
"@ccms/nashorn": "^0.17.0",
"@javatypes/amqp-client": "^0.0.3",
"@javatypes/spring-amqp": "^0.0.3",
"@javatypes/spring-rabbit": "^0.0.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/api",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -19,9 +19,9 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0",
"@ccms/polyfill": "^0.16.2",
"@ccms/common": "^0.17.0",
"@ccms/container": "^0.17.0",
"@ccms/polyfill": "^0.17.0",
"base64-js": "^1.5.1",
"source-map-builder": "^0.0.7"
},

View File

@@ -105,7 +105,7 @@ export class MiaoScriptConsole implements Console {
let sourceMappingURL = lastLine.split('sourceMappingURL=', 2)[1]
if (sourceMappingURL.startsWith('data:application/json;base64,')) {
sourceContent = String.fromCharCode(...Array.from(base64.toByteArray(sourceMappingURL.split(',', 2)[1])))
} else if (sourceMappingURL.startsWith('http')) {
} else if (sourceMappingURL.startsWith('http://') || sourceMappingURL.startsWith('https://')) {
// TODO
} else {
let file = Paths.get(Paths.get(fileName, '..', sourceMappingURL).toFile().getCanonicalPath()).toFile()

View File

@@ -73,6 +73,12 @@ export namespace server {
getService(service: string): any {
throw new Error("Method not implemented.")
}
broadcast(message: string, permission: string) {
throw new Error("Method not implemented.")
}
broadcastMessage(message: string) {
throw new Error("Method not implemented.")
}
dispatchCommand(sender: string | any, command: string): boolean {
throw new Error("Method not implemented.")
}

View File

@@ -17,7 +17,7 @@ export namespace task {
this.cacheTasks.delete(taskId)
let ownerName = task.getOwner()?.description.name
if (ownerName && this.pluginCacheTasks.has(ownerName)) {
this.pluginCacheTasks.get(ownerName).delete(taskId)
this.pluginCacheTasks.get(ownerName)?.delete(taskId)
}
})
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/bukkit",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript bukkit package",
"keywords": [
"miaoscript",
@@ -25,8 +25,8 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
"@ccms/api": "^0.17.0",
"@ccms/common": "^0.17.0",
"@ccms/container": "^0.17.0"
}
}

View File

@@ -4,7 +4,7 @@ import { provideSingleton } from '@ccms/container'
import * as reflect from '@ccms/common/dist/reflect'
import chat from './enhance/chat'
let Bukkit = org.bukkit.Bukkit
let Bukkit: typeof org.bukkit.Bukkit = org.bukkit.Bukkit
@provideSingleton(server.Server)
export class BukkitServer extends server.ReflectServer {
@@ -30,6 +30,12 @@ export class BukkitServer extends server.ReflectServer {
getService(service: string) {
return Bukkit.getServicesManager().getRegistration(base.getClass(service))?.getProvider()
}
broadcast(message: string, permission: string) {
return Bukkit.broadcast(message, permission)
}
broadcastMessage(message: string) {
return Bukkit.broadcastMessage(message)
}
dispatchCommand(sender: string | any, command: string): boolean {
if (typeof sender === 'string') {
sender = this.getPlayer(sender)

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/bungee",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript bungee package",
"keywords": [
"miaoscript",
@@ -25,8 +25,8 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
"@ccms/api": "^0.17.0",
"@ccms/common": "^0.17.0",
"@ccms/container": "^0.17.0"
}
}

View File

@@ -58,6 +58,12 @@ export class BungeeServer implements server.Server {
getService(service: string) {
throw new Error("Method not implemented.")
}
broadcast(message: string, permission: string) {
return Bungee.broadcast(message)
}
broadcastMessage(message: string) {
return Bungee.broadcast(message)
}
dispatchCommand(sender: string | any, command: string): boolean {
if (typeof sender === 'string') {
sender = this.getPlayer(sender)

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@ccms/client",
"version": "0.16.1",
"version": "0.17.0",
"description": "MiaoScript client package",
"keywords": [
"miaoscript",
@@ -25,7 +25,7 @@
"minecraft-protocol": "^1.25.0"
},
"devDependencies": {
"@types/node": "^16.3.2",
"@types/node": "^16.4.12",
"rimraf": "^3.0.2",
"typescript": "^4.3.5"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/common",
"version": "0.16.0",
"version": "0.17.0",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -19,7 +19,7 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@ccms/nashorn": "^0.16.0",
"@ccms/nashorn": "^0.17.0",
"@javatypes/jdk": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/compile",
"version": "0.16.0",
"version": "0.17.0",
"description": "MiaoScript compile package",
"keywords": [
"miaoscript",

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/container",
"version": "0.16.0",
"version": "0.17.0",
"description": "MiaoScript container package",
"keywords": [
"miaoscript",
@@ -19,7 +19,7 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@ccms/nashorn": "^0.16.0",
"@ccms/nashorn": "^0.17.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.3.5"

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/core",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -24,8 +24,8 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/container": "^0.16.0"
"@ccms/api": "^0.17.0",
"@ccms/container": "^0.17.0"
},
"gitHead": "781524f83e52cad26d7c480513e3c525df867121"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/database",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript database package",
"keywords": [
"miaoscript",
@@ -25,7 +25,7 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/container": "^0.16.0"
"@ccms/api": "^0.17.0",
"@ccms/container": "^0.17.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/i18n",
"version": "0.16.0",
"version": "0.17.0",
"description": "MiaoScript i18n package",
"keywords": [
"miaoscript",
@@ -19,7 +19,7 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@ccms/nashorn": "^0.16.0",
"@ccms/nashorn": "^0.17.0",
"@types/js-yaml": "^4.0.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/keyvalue",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript keyvalue package",
"keywords": [
"miaoscript",
@@ -19,12 +19,12 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
"@ccms/api": "^0.17.0",
"@ccms/common": "^0.17.0",
"@ccms/container": "^0.17.0"
},
"devDependencies": {
"@ccms/nashorn": "^0.16.0",
"@ccms/nashorn": "^0.17.0",
"@javatypes/amqp-client": "^0.0.3",
"@javatypes/spring-amqp": "^0.0.3",
"@javatypes/spring-rabbit": "^0.0.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/nashorn",
"version": "0.16.0",
"version": "0.17.0",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/nodejs",
"version": "0.16.0",
"version": "0.17.0",
"description": "MiaoScript nodejs package",
"keywords": [
"miaoscript",
@@ -19,7 +19,7 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@ccms/nashorn": "^0.16.0",
"@ccms/nashorn": "^0.17.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"tslib": "^2.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/nukkit",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript nukkit package",
"keywords": [
"miaoscript",
@@ -25,8 +25,8 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
"@ccms/api": "^0.17.0",
"@ccms/common": "^0.17.0",
"@ccms/container": "^0.17.0"
}
}

View File

@@ -27,6 +27,12 @@ export class NukkitServer implements server.Server {
getService(service: string) {
return Nukkit.getServiceManager().getProvider(base.getClass(service))
}
broadcast(message: string, permission: string) {
return Nukkit.broadcast(message, permission)
}
broadcastMessage(message: string) {
return Nukkit.broadcastMessage(message)
}
dispatchCommand(sender: string | any, command: string): boolean {
if (typeof sender === 'string') {
sender = this.getPlayer(sender)

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/plugin",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -25,10 +25,10 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0",
"@ccms/i18n": "^0.16.0",
"@ccms/api": "^0.17.0",
"@ccms/common": "^0.17.0",
"@ccms/container": "^0.17.0",
"@ccms/i18n": "^0.17.0",
"js-yaml": "^4.1.0",
"yaml": "^1.10.2"
}

View File

@@ -92,18 +92,10 @@ export class PluginConfigManager {
console.i18n("ms.plugin.manager.config.save.default", { plugin: plugin.description.name, name: metadata.name, format: metadata.format })
} else {
configValue = configLoader.load(base.read(metadata.file)) || {}
if (defaultValue) {
let needSave = false
for (const key of Object.keys(defaultValue)) {
// 当配置文件不存在当前属性时才进行赋值
if (!Object.prototype.hasOwnProperty.call(configValue, key)) {
configValue[key] = defaultValue[key]
needSave = true
}
}
needSave && base.save(metadata.file, configLoader.dump(configValue))
if (defaultValue && this.setDefaultValue(configValue, defaultValue)) {
base.save(metadata.file, configLoader.dump(configValue))
}
console.debug(`[${plugin.description.name}] Load Config ${metadata.variable} from file ${metadata.file} =>\n${JSON.stringify(configValue, undefined, 4)}`)
console.debug(`[${plugin.description.name}] Load Config ${metadata.variable} from file ${metadata.file} =>\n${JSON.stringify(configValue, undefined, 4).substr(0, 500)}`)
}
this.defienConfigProp(plugin, metadata, configValue)
} catch (error) {
@@ -112,12 +104,27 @@ export class PluginConfigManager {
}
}
private setDefaultValue(configValue, defaultValue) {
let needSave = false
for (const key of Object.keys(defaultValue)) {
// 当配置文件不存在当前属性时才进行赋值
if (!Object.prototype.hasOwnProperty.call(configValue, key)) {
configValue[key] = defaultValue[key]
needSave = true
} else if (Object.prototype.toString.call(configValue[key]) == "[object Object]") {
// 对象需要递归检测
needSave ||= this.setDefaultValue(configValue[key], defaultValue[key])
}
}
return needSave
}
private saveConfig0(plugin: plugin.Plugin, metadata: interfaces.ConfigMetadata) {
try {
metadata.file = fs.concat(fs.file(plugin.description.loadMetadata.file).parent, plugin.description.name, metadata.filename)
let result = this.getConfigLoader(metadata.format).dump(plugin[metadata.variable])
base.save(metadata.file, result)
console.debug(`[${plugin.description.name}] Save Config ${metadata.variable} to file ${metadata.file} =>\n${result}`)
console.debug(`[${plugin.description.name}] Save Config ${metadata.variable} to file ${metadata.file} =>\n${result.substr(0, 500)}`)
return true
} catch (error) {
console.i18n("ms.plugin.manager.config.save.error", { plugin: plugin.description.name, name: metadata.name, format: metadata.format, error })

View File

@@ -0,0 +1,35 @@
[综合|前置]MiaoLink —— 喵式映射 用于无公网环境的自动化端口映射[1.7.10+全版本]
# MiaoLink
## 插件介绍
> 自动化端口公网映射
## 图片展示
- ![启动图片](https://i.loli.net/2021/09/23/vgOlw7p4xCdJPRo.png)
## 使用方式
- 本插件依赖于 `MiaoScript` 请前往 [站内帖子](https://www.mcbbs.net/thread-774401-1-1.html) 完成安装
- 执行 `/mspm install MiaoLink` 安装 MiaoLink 脚本插件
- 访问 [圈云映射](https://nps.yumc.pw) 申请一键映射指令
- ![image.png](https://i.loli.net/2021/09/23/pMISi5RWk74g2PA.png)
- 执行网页上提供的指令 等待客户端上线
- ![image.png](https://i.loli.net/2021/09/23/aoJqZK1rbWw3G8X.png)
- 使用访问地址即可链接服务器
- ![image.png](https://i.loli.net/2021/09/23/XqPz7TftarMbmcY.png)
### Roadmap
- 支持Bukkit端自动化映射(已完成)
- 支持Bungee端自动化映射(开发中)
- 支持Sponge端自动化映射(开发中)
### 感谢
- [NPS](https://github.com/ehang-io/nps) 开源项目
- [蓝科数据](https://www.lankodata.com/aff.php?aff=32) 提供的映射节点
- [AkkoCloud](https://www.akkocloud.com/aff.php?aff=698) 提供的映射节点
#### 本插件所用所有代码均为原创,不存在借用/抄袭等行为

View File

@@ -1,5 +1,6 @@
### Miao系列插件
- [[综合|前置]MiaoLink —— 喵式映射 用于无公网环境的自动化端口映射[全版本]](https://www.mcbbs.net/thread-1121423-1-1.html)
- [[经济]MiaoReward —— 喵式奖励 让玩家看广告为服务器提供收入吧[1.7.10+全版本]](https://www.mcbbs.net/thread-1121423-1-1.html)
- [[编程]MiaoBlockly —— 喵式积木 用简单的积木来写插件吧[1.12.2+全版本]](https://www.mcbbs.net/thread-1129411-1-1.html)
- [[编程]MiaoConsole —— 喵式终端 通过MC端口直接控制服务器 调试插件[1.12.2+全版本]](https://www.mcbbs.net/thread-1129227-1-1.html)

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@ccms/plugins",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript plugins package",
"keywords": [
"miaoscript",
@@ -32,10 +32,10 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@babel/standalone": "^7.14.7",
"@ccms/api": "^0.16.2",
"@ccms/container": "^0.16.0",
"@ccms/plugin": "^0.16.2",
"crypto-js": "^4.0.0"
"@babel/standalone": "^7.15.0",
"@ccms/api": "^0.17.0",
"@ccms/container": "^0.17.0",
"@ccms/plugin": "^0.17.0",
"crypto-js": "^4.1.1"
}
}

View File

@@ -67,7 +67,7 @@ export class MiaoConsole extends interfaces.Plugin {
}
})
this.task.create(() => {
if (!this.babel) {
if (!this.babel && this.serverType != constants.ServerType.Bungee) {
try {
this.logger.console('§3脚本 Babel 引擎初始化中 请稍候...')
let startTime = Date.now()
@@ -164,7 +164,11 @@ export class MiaoConsole extends interfaces.Plugin {
if (this.rootLogger) {
let AbstractHandler = Java.type('java.util.logging.Handler')
let ProxyHandler = Java.extend(AbstractHandler, {
publish: (record) => process.emit('message', record.getMessage()),
publish: (record) => {
if (record.getLevel().intValue() > 500) {
process.emit('message', record.getMessage())
}
},
flush: () => { },
close: () => { }
})
@@ -248,18 +252,21 @@ export class MiaoConsole extends interfaces.Plugin {
checkWebSocketClient(client: SocketIOSocket) {
if (!this.token) {
this.logger.console(`§6客户端 §b${client.id} §a请求连接 §4服务器尚未设置 Token 无法连接!`)
client.emit('unauthorized', () => client.disconnect(true))
return false
return this.notifyDisconnect(client, `§6客户端 §b${client.id} §a请求连接 §4服务器尚未设置 Token 无法连接!`)
}
if (this.token != client.handshake.query.token) {
this.logger.console(`§6客户端 §b${client.id} §c无效请求 §4请提供正确Token后再次连接!`)
client.emit('unauthorized', () => client.disconnect(true))
return false
return this.notifyDisconnect(client, `§6客户端 §b${client.id} §c无效请求 §4请提供正确Token后再次连接!`)
}
return true
}
private notifyDisconnect(client: SocketIOSocket, reason: string) {
this.logger.console(reason)
client.emit('unauthorized', () => client.disconnect(true))
setTimeout(() => { if (client.connected) { client.disconnect(true) } }, 5)
return false
}
startSocketIOServer() {
let namespace = this.socketIOServer.of('/MiaoConsole')
process.on('message', (msg) => namespace.emit('log', msg))

View File

@@ -2,9 +2,10 @@
/// <reference types="@javatypes/bukkit-api" />
/// <reference types="@javatypes/sponge-api" />
import { plugin, server, task } from '@ccms/api'
import { constants, plugin, server, task } from '@ccms/api'
import { Autowired } from '@ccms/container'
import { Config, interfaces, JSPlugin, PluginConfig } from '@ccms/plugin'
import { Config, disable, enable, interfaces, JSPlugin, PluginConfig } from '@ccms/plugin'
import { client } from '@ccms/websocket'
import http from '@ccms/common/dist/http'
@@ -26,7 +27,7 @@ const defaultDataConfig = {
server_total_entities: "%server_total_entities%",
}
@JSPlugin({ prefix: 'Dashboard', version: '1.0.0', author: 'MiaoWoo', source: __filename, depends: ['MiaoConsole'] })
@JSPlugin({ prefix: 'Dashboard', version: '1.0.0', author: 'MiaoWoo', depends: ['MiaoConsole'], source: __filename })
export class MiaoDashboard extends interfaces.Plugin {
@Autowired()
private server: server.Server
@@ -44,7 +45,8 @@ export class MiaoDashboard extends interfaces.Plugin {
@Config()
private dataConfig: PluginConfig & typeof defaultDataConfig = defaultDataConfig
@Config({ autosave: true })
private dataCache: { [key: string]: { time: string, value: Number }[] } = {}
private dataCache: PluginConfig & { [key: string]: { time: string, value: Number }[] } = {}
private statisticTimer: task.Task
private PlaceholderAPI: { setPlaceholders: (player: any, str: string) => string }
@@ -66,6 +68,10 @@ export class MiaoDashboard extends interfaces.Plugin {
}
})
})
}
@enable({ servers: [constants.ServerType.Bukkit] })
enableBukkit() {
this.PlaceholderAPI = base.getClass("me.clip.placeholderapi.PlaceholderAPI").static
this.statisticTimer = this.taskManager.create(() => {
for (const key of Object.keys(this.dataConfig)) {
@@ -79,6 +85,41 @@ export class MiaoDashboard extends interfaces.Plugin {
}
}
}, this).async().timer(20 * 10).submit()
this.proxys = client.io('ws://192.168.2.25:25577/MiaoConsole?access_token=325325', {
path: "/ws"
})
this.proxys.on('connect', () => {
this.logger.info('connect')
this.proxys.emit('type', (type) => {
console.log('server type is ' + type)
})
})
this.proxys.on('log', (msg) => {
console.log(msg)
})
}
private proxys
@enable({ servers: [constants.ServerType.Bungee] })
enbaleBungee() {
this.proxys = client.io('ws://192.168.2.25:25565/MiaoConsole?access_token=325325', {
path: "/ws"
})
this.proxys.on('connect', () => {
this.logger.info('connect')
this.proxys.emit('type', (type) => {
console.log('server type is ' + type)
})
})
this.proxys.on('log', (msg) => {
console.log(msg)
})
}
@disable({ servers: [constants.ServerType.Bungee] })
disableBungee() {
}
private dateFormat(fmt: string, date = new Date()) {
@@ -103,7 +144,7 @@ export class MiaoDashboard extends interfaces.Plugin {
disable() {
this.namespace?.close()
this.statisticTimer.cancel()
this.statisticTimer?.cancel()
}
private wrapper(fn, data) {

View File

@@ -0,0 +1,155 @@
/// <reference types="@javatypes/bungee-api" />
/// <reference types="@javatypes/bukkit-api" />
/// <reference types="@javatypes/sponge-api" />
import { server, task } from '@ccms/api'
import { Autowired } from '@ccms/container'
import { Cmd, JSPlugin, Tab, interfaces, PluginConfig, Config } from '@ccms/plugin'
import * as fs from '@ccms/common/dist/fs'
import http from '@ccms/common/dist/http'
import * as base64 from 'base64-js'
const Runtime: typeof java.lang.Runtime = Java.type('java.lang.Runtime')
const Thread = Java.type('java.lang.Thread')
const defaultConfig = {
id: 0,
vkey: ''
}
@JSPlugin({ name: 'MiaoLink', version: '1.0.2', author: 'MiaoWoo', source: __filename })
export class MiaoLink extends interfaces.Plugin {
@Autowired(task.TaskManager)
private task: task.TaskManager
@Autowired(server.Server)
private server: server.Server
@Config()
private config: PluginConfig & typeof defaultConfig = defaultConfig
private isWindows = false
private clientName: string = 'npc'
private client: string = ''
private port: number = 0
private npc: any
load() {
this.isWindows = process.platform == 'win32' || process.platform.toLowerCase().startsWith('windows')
if (this.isWindows) {
this.logger.console('§a当前运行于Windows服务器...')
this.clientName = "npc.exe"
} else {
this.logger.console('§a当前运行于Linux服务器...')
}
}
bukkitload() {
this.port = org.bukkit.Bukkit.getPort()
}
spongeload() {
this.logger.console('§4Sponge暂不支持端口映射!')
}
bungeeload() {
let server: net.md_5.bungee.api.ProxyServer = base.getInstance().getProxyServer()
this.port = server.getConfig().getListeners()[0].getQueryPort()
}
enable() {
if (!this.config.vkey) {
return this.logger.console('§4服务器尚未绑定 取消自动映射!')
}
this.cmdconnect(this.server.getConsoleSender())
}
disable() {
this.cmddisconnect(this.server.getConsoleSender())
}
@Cmd({ autoMain: true })
mlink() { }
cmdconnect(sender: any, secret?: string) {
if (secret) {
let configStr = String.fromCharCode(...Array.from(base64.toByteArray(secret)))
let config = JSON.parse(configStr)
this.config.id = config.id
this.config.vkey = config.vkey
this.config.save()
}
this.startClient(sender)
}
cmddisconnect(sender: any) {
if (!this.npc || !this.npc.isAlive()) {
return this.logger.sender(sender, '§4客户端尚未运行 跳过关闭流程...')
}
this.logger.sender(sender, '§6已发送关闭客户端指令...')
this.npc.destroy()
}
@Tab()
tabmlink(_sender: any, _command: string, _args: string[]) {
}
startClient(sender: any, id: number = this.config.id, vkey: string = this.config.vkey) {
if (!this.port) {
return this.logger.sender(sender, '§4服务器端口获取失败 取消自动映射!')
}
if (!id || !vkey) {
return this.logger.sender(sender, '§4服务器尚未配置 取消自动映射!')
}
if (this.npc && this.npc.isAlive()) {
this.npc.destroy()
}
this.task.create(() => {
this.logger.sender(sender, `§6获取到服务器端口: §3${this.port} §a开始映射端口!`)
let client = this.query(id, vkey, this.port)
let node = client.node
let tunnel = client.tunnel
this.client = fs.concat(__dirname, 'MiaoLink', this.clientName)
this.download(sender)
try {
this.npc = Runtime.getRuntime().exec(`${this.client} -server=${node.bridge} -vkey=${vkey} -type=tcp`)
this.logger.sender(sender, `§a服务器端口映射成功! §6访问地址: §3${node.address}:${tunnel.port}`)
return this.logger.console(`§4客户端已结束运行 退出代码: ${this.npc.waitFor()} 映射关闭!`)
} catch (error) {
this.logger.sender(sender, `§c服务器端口映射失败! §4ERROR: ${error}`)
console.ex(error)
}
}, this).async().later(5).submit()
}
download(sender: any) {
try {
if (!fs.exists(this.client)) {
this.logger.sender(sender, '§c客户端文件不存在 开始下载客户端...')
let temp = this.client + '.tmp'
http.download("https://static.c5mc.cn/" + this.clientName, temp)
fs.move(temp, this.client, true)
if (!this.isWindows) {
this.logger.sender(sender, '§a当前处于Linux环境 赋予可执行权限...')
Runtime.getRuntime().exec(`chmod +x ${this.client}`)
}
}
} catch (error) {
Thread.sleep(500)
this.download(sender)
}
}
query(id: number, vkey: string, target: number) {
let result = this.post(`/client?id=${id}&vkey=${vkey}&target=${target}`)
if (result.code != 200) {
throw new Error('§4客户端查询失败: ' + result.msg)
}
return result.data
}
post(path, data = {}) {
return http.post("https://nps.yumc.pw/api" + path, data)
}
}

View File

@@ -2,11 +2,11 @@
/// <reference types="@javatypes/bukkit-api" />
/// <reference types="@javatypes/sponge-api" />
import { particle } from '@ccms/api'
import { constants, particle } from '@ccms/api'
import { Autowired } from '@ccms/container'
import { Cmd, JSPlugin, Tab, interfaces } from '@ccms/plugin'
@JSPlugin({ version: '1.0.0', author: 'MiaoWoo', source: __filename })
@JSPlugin({ version: '1.0.0', author: 'MiaoWoo', servers: [constants.ServerType.Bukkit], source: __filename })
export class MiaoParticle extends interfaces.Plugin {
@Autowired()
private particleManager: particle.ParticleManager

View File

@@ -10,13 +10,8 @@ import type { MiaoReward } from './MiaoReward'
import http from '@ccms/common/dist/http'
import * as CryptoJS from "crypto-js"
const Thread = java.lang.Thread
const Thread = Java.type('java.lang.Thread')
interface PlayerPointsAPI {
look(name: string)
give(name: string, amount: number)
take(name: string, amount: number)
}
interface App {
appid: string
appname: string
@@ -78,7 +73,7 @@ const defaultConfig = {
}
}
@JSPlugin({ version: '1.5.0', author: 'MiaoWoo', source: __filename, servers: [constants.ServerType.Bukkit], depends: ['MiaoReward'], nativeDepends: ['PlaceholderAPI', 'ProtocolLib'] })
@JSPlugin({ version: '1.6.6', author: 'MiaoWoo', source: __filename, servers: [constants.ServerType.Bukkit], depends: ['MiaoReward'], nativeDepends: ['PlaceholderAPI', 'ProtocolLib'] })
export class MiaoPay extends interfaces.Plugin {
@Autowired()
private server: server.Server
@@ -109,14 +104,22 @@ export class MiaoPay extends interfaces.Plugin {
enable() {
if (!this.MiaoReward) { return this.logger.error('当前脚本插件需要 MiaoReward 作为前置脚本插件!') }
if (!this.config.id || !this.config.secret) { return this.logger.console('§4尚未配置商户信息 将无法正常收款!') }
let info = this.httpPost('/apps', { id: this.config.id })
this.initAppInfo()
}
private initAppInfo() {
let info = this.httpPost('/apps', { id: this.config.id }, 10)
if (info.code == 200) {
this.appInfo = info.data
this.config.ratio = this.appInfo.ratio
this.config.coinName = this.appInfo.coin_name
if (this.config.name == this.appInfo.appname) {
this.config.name = ''
this.config.save()
}
} else {
this.logger.console('§4初始化支付系统失败 请检查配置是否正!')
this.logger.console('§c服务器返回异常: §4' + info.msg)
this.logger.console('§4初始化支付系统失败 请检查配置或网络是否正!')
this.logger.console('§c返回异常: §4' + info.msg)
}
}
@@ -132,14 +135,17 @@ export class MiaoPay extends interfaces.Plugin {
cmdpay(sender: org.bukkit.entity.Player, amount: number = 0) {
if (!sender.getItemInHand) { return this.logger.sender(sender, '§4控制台无法执行此命令!') }
if (!this.appInfo) {
return this.logger.sender(sender, '§4当前服务器尚未配置 请联系管理员配置MiaoPay!')
}
if (!this.config.id || !this.config.secret) { return this.logger.sender(sender, '§c当前服务器尚未配置 请联系管理员配置支付密钥!') }
if (!this.appInfo) {
this.initAppInfo()
return this.logger.sender(sender, '§6支付系统初始化中 请稍候重试...')
}
if (this.cacheMap.has(sender.getName())) {
this.logger.sender(sender, '§c您有一笔订单尚未完成 请完成支付或等待订单超时!')
let sync = this.cacheSyncMap.get(sender.getName())
if (!sync.cancelled) { return }
if (sync.scaned) { return }
sync.scaned = true
Thread.sleep(1100)
sync.scaned = false
sync.left = (sync.paying ? 100 : 55) - (Math.round(Date.now() / 1000) - sync.start)
let order = this.cacheMap.get(sender.getName())
@@ -147,13 +153,17 @@ export class MiaoPay extends interfaces.Plugin {
return
}
if (amount < 1) { return this.logger.sender(sender, `§c充值异常 §4充值金额不得小于 1 ${this.config.coinName}!`) }
if (amount / this.config.ratio > 5000) { return this.logger.sender(sender, `§c充值异常 §4充值金额不得大于 ${this.config.ratio * 5000} ${this.config.coinName}!`) }
if (amount / this.config.ratio > 5000) { return this.logger.sender(sender, `§c充值异常 §4充值金额不得大于 ${this.config.ratio * 3000} ${this.config.coinName}!`) }
if (amount != Math.round(amount)) { return this.logger.sender(sender, `§c充值异常 §4充值金额必须为整数!`) }
try {
this.getPlayerAmount(sender)
} catch (error) {
return this.logger.sender(sender, error.message || error)
}
this.taskManager.create(() => this.createOrderByPlayer(sender, amount)).async().submit()
}
private createOrderByPlayer(sender: org.bukkit.entity.Player, amount: number = 0) {
this.MiaoReward.sendTitle(sender, `§6充值 §a${amount} §6${this.config.coinName}`, '§c正在请求充值二维码 请稍候...')
let sync: any = { scaned: false, start: Math.round(Date.now() / 1000) }
let order = this.createOrder(sender, amount)
@@ -193,27 +203,6 @@ export class MiaoPay extends interfaces.Plugin {
this.MiaoReward.clearTitle(sender)
}
cmdquery(sender: org.bukkit.entity.Player, id: string) {
if (!id) { return this.logger.sender(sender, '§c请输入订单号!') }
this.taskManager.create(() => {
let result = this.queryOrder(id, sender.getName(), sender.getUniqueId().toString())
if (result.code != 200) { return this.logger.sender(sender, `§c查询异常! §4ERROR: ${result.msg}`) }
let order = result.data
this.logger.sender(sender, [
`§6商户名称: §3${order.appname}`,
`§6订单号: §3${id}`,
`§6商品: §b${order.subject}`,
`§6金额: §e${order.amount}`,
`§6玩家: §a${order.username}`,
`§6状态: §c${order.status}`,
])
if (order.status > 1 && order.status < 4) {
this.logger.sender(sender, `§3当前订单已支付 尚未完成充值 开始补单操作...`)
this.recharge(sender, order)
}
}).async().submit()
}
cmdcheck(sender: org.bukkit.entity.Player, force = 1) {
if (this.checkSet.has(sender.getName())) {
return this.logger.sender(sender, '§c检查任务执行中 请稍候...')
@@ -246,16 +235,18 @@ export class MiaoPay extends interfaces.Plugin {
let order_id = order.order_id
let amount = order.amount
let point = this.safeMultiply(amount, this.config.ratio)
if (!sender.isOnline()) { return }
let finish = this.preFinishOrder(order_id)
if (finish.code != 200) {
this.sendError(sender, order_id, amount, '§4充值预标记异常!')
this.sendError(sender, order_id, amount, '充值预标记异常!')
return this.logger.console(`§c充值系统异常 订单 §3${order_id} 预标记异常! §4${this.config.coinName}已停止充值 §c请手动补单!`)
}
this.taskManager.callSyncMethod(() => {
if (!sender.isOnline()) { return this.errorOrder(order_id, "充值前玩家掉线 请重置标记!") }
let prePoint = this.getPlayerAmount(sender)
let command = this.config.command.replace('%player_name%', sender.getName()).replace('%amount%', `${point}`).replace('%remark%', `${order_id}`)
if (!this.server.dispatchConsoleCommand(command)) {
return this.sendError(sender, order_id, amount, '§4充值命令执行异常!')
return this.sendError(sender, order_id, amount, '充值命令执行异常!')
}
this.checkRecharge(sender, order_id, amount, prePoint, point)
})
@@ -269,10 +260,9 @@ export class MiaoPay extends interfaces.Plugin {
private checkRecharge(sender: org.bukkit.entity.Player, order_id: string, amount: number, prePoint: number, point: number) {
this.taskManager.create(() => {
if (!sender.isOnline()) { return this.errorOrder(order_id, "充值后玩家掉线 请标记已兑换!") }
let nowPoint = this.checkNowPoint(sender, point, prePoint)
if (nowPoint === false) {
return this.sendError(sender, order_id, amount, '§4充值结果检测异常!')
}
if (nowPoint === false) { return this.sendError(sender, order_id, amount, '充值结果检测异常!') }
this.logger.sender(sender, [
`§6充值 §a${point} §6${this.config.coinName} §a成功 §6当前账户余额: §3${nowPoint} §6${this.config.coinName}`,
`§c如出现未到账的情况 请联系管理员!`
@@ -280,29 +270,18 @@ export class MiaoPay extends interfaces.Plugin {
this.rewardOrder(sender, order_id, point)
let finish = this.finishOrder(order_id)
if (finish.code != 200) {
return this.logger.console(`§c充值系统异常 订单 §3${order_id} 完成标记异常! §4${this.config.coinName}可能重复到账!`)
this.errorOrder(order_id, '充值完成标记异常 请到后台标记为已兑换!')
return this.logger.console(`§c充值系统异常 订单 §3${order_id} §c完成标记异常! §a请到后台标记为已兑换! §4否则${this.config.coinName}可能重复到账!`)
}
}).async().submit()
}
private checkNowPoint(sender: org.bukkit.entity.Player, point: number, prePoint: number) {
private checkNowPoint(sender: org.bukkit.entity.Player, point: number, prePoint: number, times: number = 1) {
if (times > 3) { return false }
let nowPoint = this.getPlayerAmount(sender)
if (nowPoint == prePoint + point) {
return nowPoint
}
Thread.sleep(100)
nowPoint = this.getPlayerAmount(sender)
if (nowPoint == prePoint + point) {
return nowPoint
}
Thread.sleep(200)
nowPoint = this.getPlayerAmount(sender)
if (nowPoint == prePoint + point) {
return nowPoint
}
Thread.sleep(300)
nowPoint = this.getPlayerAmount(sender)
return false
if (nowPoint == prePoint + point) { return nowPoint }
Thread.sleep(times * 100)
return this.checkNowPoint(sender, point, prePoint, times++)
}
private rewardOrder(sender, order_id, point) {
@@ -333,7 +312,7 @@ export class MiaoPay extends interfaces.Plugin {
}
sendError(sender: org.bukkit.entity.Player, order_id: string, amount: number, error: string) {
return this.logger.sender(sender, [
this.logger.sender(sender, [
`§c========== ${this.config.prefix}§4充值异常 §c==========`,
`§6异常订单: §3${order_id}`,
`§6订单金额: §3${amount}`,
@@ -341,8 +320,10 @@ export class MiaoPay extends interfaces.Plugin {
`§6异常账号: §b${sender.getName()}`,
`§6异常时间: §a${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
`§c如果已付款但${this.config.coinName}未到账 请截图发给腐竹!`,
`§c可尝试重新登录 或 执行 §3/mpay check §c手动补单!`,
`§c========== ${this.config.prefix}§4充值异常 §c==========`,
])
this.errorOrder(order_id, error)
}
@Tab()
@@ -367,15 +348,19 @@ export class MiaoPay extends interfaces.Plugin {
}
private preFinishOrder(id: string) {
return this.httpPost('/preFinish', { id })
return this.httpPost('/preFinish', { id }, 3)
}
private errorOrder(id: string, error: string) {
return this.httpPost('/error', { id, error }, 3)
}
private finishOrder(id: string) {
return this.httpPost('/finish', { id })
return this.httpPost('/finish', { id }, 3)
}
private createOrder(sender: org.bukkit.entity.Player, amount: number): Order {
let serverName = this.appInfo?.appname
let serverName = this.appInfo.appname
if (this.config.name) { serverName = `${serverName}(${this.config.name})` }
let result = this.httpPost('/create', {
subject: `${serverName} 充值 ${amount} ${this.config.coinName}`,
@@ -390,28 +375,38 @@ export class MiaoPay extends interfaces.Plugin {
}
private queryOrder(id: string, username: string, uuid: string) {
return this.httpPost('/query', { id, username, uuid })
return this.httpPost('/query', { id, username, uuid }, 2)
}
private queryUnconverted(username: string, force: number) {
return this.httpPost('/unconverted', { username, force })
return this.httpPost('/unconverted', { username, force }, 2)
}
private httpPost(method: string, data: any) {
private httpPost(method: string, data: any, retry = 0) {
let startTime = Date.now()
data.appid = this.config.id
data.timestamp = Math.round(Date.now() / 1000)
data.nonce = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/x/g, () => (Math.random() * 16 | 0).toString(16))
data.sign = this.sign(data)
let url = `${this.apiGateWay}/api${method}`
let result = http.post(url, data)
console.debug(`
try {
let result = http.post(url, data)
console.debug(`
====== HTTP POST ======
REQUEST URL : ${url}
REQUEST DATA: ${JSON.stringify(data)}
RESPONSE : ${JSON.stringify(result)}
CAST TIME : ${Date.now() - startTime}`)
return result
return result
} catch (error) {
if (retry) {
Thread.sleep(retry * 10)
return this.httpPost(method, data, --retry)
}
console.console('§4请求支付中心发生异常 请联系管理员处理此问题!')
console.ex(error)
return { code: 500, msg: '本地网络错误: ' + error.message, data: error }
}
}
private http_build_query(params: any) {

View File

@@ -2,7 +2,7 @@
/// <reference types="@javatypes/bukkit-api" />
/// <reference types="@javatypes/sponge-api" />
import { plugin, server, task } from '@ccms/api'
import { constants, plugin, server, task } from '@ccms/api'
import { Autowired, JSClass } from '@ccms/container'
import { Cmd, Config, interfaces, JSPlugin, Listener, PluginConfig, Tab } from '@ccms/plugin'
@@ -37,7 +37,7 @@ const defaultConfig = {
}
}
@JSPlugin({ version: '1.0.1', author: 'MiaoWoo', source: __filename, depends: ['MiaoReward'], nativeDepends: ['PlaceholderAPI'] })
@JSPlugin({ version: '1.0.1', author: 'MiaoWoo', servers: [constants.ServerType.Bukkit], depends: ['MiaoReward'], nativeDepends: ['PlaceholderAPI'], source: __filename })
export class MiaoRebate extends interfaces.Plugin {
@Autowired()
private server: server.Server

View File

@@ -663,6 +663,7 @@ export class MiaoReward extends interfaces.Plugin {
sync.cancelled = false
let task = this.taskManager.create(() => {
try {
console.log(JSON.stringify(sync))
if (sync.scaned || !sender.isOnline() || !this.isHoldQrCodeItem(sender) || --sync.left < 0) {
if (sync.left < 0) {
this.logger.sender(sender, '§c二维码已过期 请重新获取 如已扫码请忽略!')

View File

@@ -1,41 +1,152 @@
/// <reference types="@javatypes/bungee-api" />
/// <reference types="@javatypes/bukkit-api" />
/// <reference types="@javatypes/sponge-api" />
/// <reference types="typescript" />
// @ts-ignore
require.clear('websocket/client')
import { server } from '@ccms/api'
import { Autowired, Container, ContainerInstance } from '@ccms/container'
import { Cmd, JSPlugin, Tab, interfaces, PluginConfig, Config } from '@ccms/plugin'
import { EventEmitter } from 'events'
import { constants, server } from '@ccms/api'
import { Autowired, JSClass } from '@ccms/container'
import { Cmd, JSPlugin, Tab, interfaces, PluginConfig, Config, Listener } from '@ccms/plugin'
import { WebSocket } from '@ccms/websocket'
const Thread = Java.type('java.lang.Thread')
const ChatColor = Java.type('org.bukkit.ChatColor')
const defaultConfig = {
version: 1,
address: '',
token: ''
token: '',
group_id: '',
admin_id: '',
message: {
join: "玩家: %player_name% 加入了服务器!",
quit: "玩家: %player_name% 退出了服务器!",
chat: "%player_name%: ",
group: "&6[&c服务器群&6] &b%sender_nickname%&6(&a%sender_user_id%&6)&r: "
}
}
//https://github3.mk-proxy.ml/-----https://github.com/Mrs4s/go-cqhttp/releases/download/v0.9.34/go-cqhttp-v0.9.34-linux-amd64
@JSPlugin({ version: '1.0.0', author: 'MiaoWoo', source: __filename })
interface RobotConfig {
address: string,
token: string,
timeout: number
}
interface PlaceholderAPI {
registerPlaceholderHook: (key: string, onPlaceholderRequest: (player, s) => string) => void
unregisterPlaceholderHook: (key: string) => void
setPlaceholders: (player: any, str: string) => string
}
class Robot extends EventEmitter {
private config: RobotConfig
private websocket: WebSocket
private invokeCount = 1;
private apiResultCache = [];
constructor(config: RobotConfig) {
super()
this.config = config
}
sleep(ms) {
Thread.sleep(ms)
}
invoke(action, params) {
if (this.websocket.readyState != WebSocket.OPEN) { throw new Error('client not connect!') }
let startTime: number = new Date().getTime()
let request = { action, params, echo: this.invokeCount++ }
this.websocket.send(JSON.stringify(request))
while (startTime + this.config.timeout > new Date().getTime()) {
if (this.apiResultCache[request.echo]) {
let result = this.apiResultCache[request.echo]
delete this.apiResultCache[request.echo]
if ((result.status === "ok" && result.retcode !== 0) && (result.status === "async" && result !== 1)) {
throw Error(`Invoke API Error! Response ${JSON.stringify(result)}`)
}
return result.data
}
this.sleep(50)
}
throw Error(`Invoke API Timeout! Request ${JSON.stringify(request)}`)
}
connect() {
this.websocket = new WebSocket(this.config.address, '', { Authorization: `Bearer ${this.config.token}` })
this.websocket.onopen = () => {
this.emit('connect')
}
this.websocket.onmessage = (event) => {
let robotEvent = JSON.parse(event.data)
if (robotEvent.post_type == "meta_event") { return }
if (robotEvent.post_type) {
this.emit(robotEvent.post_type, robotEvent)
}
}
this.websocket.onclose = (event) => {
this.emit('close', event)
}
this.websocket.onerror = (event) => {
this.emit('error', event)
}
}
disconnect(reason = '') {
if (this.websocket) {
this.websocket.close(0, reason)
}
}
sendGroupMessage(group_id, message) {
this.websocket.send(JSON.stringify({
action: "send_msg",
params: { group_id, message }
}))
}
sendPrivateMessage(user_id, message) {
this.websocket.send(JSON.stringify({
action: "send_msg",
params: { user_id, message }
}))
}
}
@JSPlugin({ version: '1.0.0', author: 'MiaoWoo', servers: [constants.ServerType.Bukkit], source: __filename, nativeDepends: ['PlaceholderAPI'] })
export class MiaoRobot extends interfaces.Plugin {
@Autowired()
private server: server.Server
private client: WebSocket
@JSClass('me.clip.placeholderapi.PlaceholderAPI')
private PlaceholderAPI: PlaceholderAPI
private robot: Robot
@Config()
private config: PluginConfig & typeof defaultConfig = defaultConfig
load() {
}
private downloadRobot() {
//https://api.github.com/repos/Mrs4s/go-cqhttp/releases?per_page=1&page=1
this.logger.prefix = ''
}
enable() {
if (this.config.address && this.config.token) {
this.cmdconnect(this.server.getConsoleSender())
if (!this.config.group_id) {
this.logger.console('§c机器人尚未配置绑定服务器群 部分功能将无法使用!')
}
} else {
this.logger.console('§c机器人尚未配置 请参照帖子内容配置机器人!')
}
}
disable() {
this.cmdclose(this.server.getConsoleSender())
if (this.robot) {
this.cmdclose(this.server.getConsoleSender())
}
}
@Cmd({ autoMain: true })
@@ -46,50 +157,64 @@ export class MiaoRobot extends interfaces.Plugin {
return this.logger.sender(sender, '§4错误 请配置服务器地址和Token!')
}
this.cmdclose(sender)
try {
this.client = new WebSocket(address, '', { Authorization: `Bearer ${token}` })
this.initRobot(this.client)
} catch (error) {
console.ex(error)
}
this.initRobot(sender)
}
private initRobot(client: WebSocket) {
client.onopen = () => {
this.logger.console(`§3连接到 §b${client.url} §a成功!`)
}
client.onmessage = (event) => {
let messageEvent = JSON.parse(event.data)
switch (messageEvent.post_type) {
case "message":
this.logger.console(`§6接收到 §3群 §b${messageEvent.group_id} §2成员 §a${messageEvent.sender.nickname} §6的消息: §r${messageEvent.message}`)
break
initRobot(sender) {
this.robot = new Robot({ ...this.config, timeout: 60 })
this.robot.on('connect', () => {
this.logger.sender(sender, '§a机器人链接成功!')
})
this.robot.on('message', (event) => {
if (event.message_type == "group" && event.group_id == this.config.group_id) {
let message: string = event.message
message = message.replace(/.*\[CQ:image\,file=(.*),url=(.*),.*]/g, '[图片]')
message = this.config.message.group
.replace(/%sender_nickname%/g, event.sender.nickname)
.replace(/%sender_card%/g, event.sender.card)
.replace(/%sender_title%/g, event.sender.title)
.replace(/%sender_user_id%/g, event.sender.user_id) + message
message = ChatColor.translateAlternateColorCodes('&', message)
this.server.getOnlinePlayers().forEach(p => this.logger.sender(p, message))
this.logger.console(message)
}
}
client.onclose = (event) => {
this.logger.console(`§4连接已断开 §6Code: §3${event.code} §6原因: §c${event.reason}!`)
}
client.onerror = (event) => {
this.logger.console(`§4发生错误: §r${event.error}`)
console.ex(event.error)
}
})
this.robot.connect()
}
cmdclose(sender: org.bukkit.entity.Player) {
if (this.client && this.client.readyState != WebSocket.CLOSED) {
this.client.close(0, 'plugin close socket')
if (this.robot) {
this.robot.disconnect()
this.logger.sender(sender, '§c机器人已断开链接!')
}
}
cmdsend(sender: org.bukkit.entity.Player, text: string) {
if (this.client) {
this.client.send(text)
this.logger.sender(sender, '§a发送成功!')
}
this?.robot.sendGroupMessage(this.config.group_id, text)
this.logger.sender(sender, '§a发送成功!')
}
@Tab()
tabmbot(_sender: any, _command: string, _args: string[]) {
return []
}
@Listener()
private PlayerJoinEvent(event: org.bukkit.event.player.PlayerJoinEvent) {
if (this.robot && this.config.group_id) {
this.robot.sendGroupMessage(this.config.group_id, this.PlaceholderAPI.setPlaceholders(event.getPlayer(), this.config.message.join))
}
}
@Listener()
private PlayerQuitEvent(event: org.bukkit.event.player.PlayerQuitEvent) {
if (this.robot && this.config.group_id) {
this.robot.sendGroupMessage(this.config.group_id, this.PlaceholderAPI.setPlaceholders(event.getPlayer(), this.config.message.quit))
}
}
@Listener()
private AsyncPlayerChatEvent(event: org.bukkit.event.player.AsyncPlayerChatEvent) {
if (this.robot && this.config.group_id) {
this.robot.sendGroupMessage(this.config.group_id, this.PlaceholderAPI.setPlaceholders(event.getPlayer(), this.config.message.chat) + event.getMessage())
}
}
}

View File

@@ -233,7 +233,7 @@ export class MiaoScriptPackageManager extends interfaces.Plugin {
this.logger.sender(sender, `§6[§3BPM§6][§a${this.serverName}§6] §6命令 §b/mspm ${args.join?.(' ')} §a发布成功!`)
}
@Cmd({ servers: [constants.ServerType.Bungee] })
@Cmd({ alias: ["bmspm"], servers: [constants.ServerType.Bungee] })
bungeemspm(sender: any, command: string, args: string[]) {
if (!sender.hasPermission('mspm.admin')) { return this.i18n(sender, 'main.command.no.permission') }
this.taskManager.create(() => this.main(sender, command, args)).async().submit()
@@ -437,12 +437,23 @@ export class MiaoScriptPackageManager extends interfaces.Plugin {
this.i18n(sender, 'prun.script', { name })
this.i18n(sender, 'run.script', { script })
let result = this.runCode(script, sender, this.pluginManager.getPlugins().get(name))
this.i18n(sender, 'run.result', { result: result == undefined ? this.translate.translate('run.noresult') : typeof result == "string" ? result : JSON.stringify(result) })
this.i18n(sender, 'run.result', { result: result == undefined ? this.translate.translate('run.noresult') : typeof result == "string" ? result : this.stringify(result) })
} catch (ex) {
this.logger.sender(sender, this.logger.stack(ex))
}
}
private stringify(object) {
let seen = []
return JSON.stringify(object, function (key, val) {
if (typeof val == "object") {
if (seen.indexOf(val) >= 0) return
seen.push(val)
}
return val
})
}
private runCode(code: string, sender: any, _this: any) {
let paramNames = [
'sender',
@@ -468,7 +479,7 @@ return eval(${JSON.stringify(code)});`)
return tfunc.apply(_this, params)
}
cmddeploy(sender: string, name: string, changelog: string) {
cmddeploy(sender: string, name: string, changelog: string = '') {
if (!process.env.AccessToken) { return this.i18n(sender, 'deploy.token.not.exists') }
this.taskManager.create(() => {
if (this.checkPlugin(sender, name)) {
@@ -533,9 +544,9 @@ return eval(${JSON.stringify(code)});`)
for (const pl of result.data) { this.packageCache[pl.name] = pl }
this.packageNameCache = Object.keys(this.packageCache)
this.i18n(sender, 'cloud.update.finish', { length: this.packageNameCache.length })
let updateCount = 0
this.pluginManager.getPlugins().forEach(p => {
let cloudPlugin = this.packageCache[p.description.name]
let updateCount = 0
//§6插件名称: §b{name}\n§6版本: §a{version}\n§6作者: §3{author}\§6更新时间: §9{updated_at}
if (cloudPlugin && cloudPlugin.version != p.description.version) {
this.i18n(sender, 'cloud.update.exists', {
@@ -546,10 +557,10 @@ return eval(${JSON.stringify(code)});`)
})
updateCount++
}
if (updateCount) {
this.i18n(sender, 'cloud.update.tip', { count: updateCount })
}
})
if (updateCount) {
this.i18n(sender, 'cloud.update.tip', { count: updateCount })
}
}).async().submit()
}
@@ -559,7 +570,7 @@ return eval(${JSON.stringify(code)});`)
this.i18n(sender, 'download.start', { name, version: pluginPkg.version })
this.i18n(sender, 'download.url', { url: pluginPkg.url })
let pluginFile = update ? fs.concat(root, this.pluginFolder, 'update', name + '.js') : fs.concat(root, this.pluginFolder, name + '.js')
http.download(pluginPkg.url, pluginFile)
http.download(pluginPkg.url + '?t=' + Date.now(), pluginFile)
this.i18n(sender, 'download.finish', { name, version: pluginPkg.version })
callback?.()
}).async().submit()

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/polyfill",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript polyfill package",
"author": "MiaoWoo <admin@yumc.pw>",
"homepage": "https://github.com/circlecloud/ms.git",
@@ -14,12 +14,12 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"@ccms/i18n": "^0.16.0",
"@ccms/nodejs": "^0.16.0",
"core-js": "^3.15.2"
"@ccms/i18n": "^0.17.0",
"@ccms/nodejs": "^0.17.0",
"core-js": "^3.16.0"
},
"devDependencies": {
"@ccms/nashorn": "^0.16.0",
"@ccms/nashorn": "^0.17.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.3.5"

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/protocol",
"version": "0.16.0",
"version": "0.17.0",
"description": "MiaoScript protocol package",
"keywords": [
"miaoscript",

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/sponge",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -25,8 +25,8 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
"@ccms/api": "^0.17.0",
"@ccms/common": "^0.17.0",
"@ccms/container": "^0.17.0"
}
}

View File

@@ -3,7 +3,8 @@ import { provideSingleton } from '@ccms/container'
import * as reflect from '@ccms/common/dist/reflect'
const Sponge = org.spongepowered.api.Sponge
const Sponge: typeof org.spongepowered.api.Sponge = org.spongepowered.api.Sponge
const Text: typeof org.spongepowered.api.text.Text = org.spongepowered.api.text.Text
const File = Java.type("java.io.File")
@provideSingleton(server.Server)
@@ -30,6 +31,12 @@ export class SpongeServer extends server.ReflectServer {
getService(service: string) {
return Sponge.getServiceManager().provide(base.getClass(service)).orElse(null)
}
broadcast(message: string, permission: string) {
return Sponge.getServer().getBroadcastChannel().permission(permission).send(Text.of(message) as any)
}
broadcastMessage(message: string) {
return Sponge.getServer().getBroadcastChannel().TO_ALL.send(Text.of(message) as any)
}
dispatchCommand(sender: string | any, command: string): boolean {
if (typeof sender === 'string') {
sender = this.getPlayer(sender)

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/spring",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript spring package",
"keywords": [
"miaoscript",
@@ -24,9 +24,9 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0",
"@ccms/database": "^0.16.2"
"@ccms/api": "^0.17.0",
"@ccms/common": "^0.17.0",
"@ccms/container": "^0.17.0",
"@ccms/database": "^0.17.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/web",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript web package",
"keywords": [
"miaoscript",
@@ -29,7 +29,7 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.16.2",
"@ccms/container": "^0.16.0"
"@ccms/api": "^0.17.0",
"@ccms/container": "^0.17.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/websocket",
"version": "0.16.2",
"version": "0.17.0",
"description": "MiaoScript websocket package",
"keywords": [
"miaoscript",
@@ -18,8 +18,12 @@
"build": "yarn clean && tsc",
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"backo2": "^1.0.2",
"parseuri": "^0.0.6"
},
"devDependencies": {
"@ccms/nashorn": "^0.16.0",
"@ccms/nashorn": "^0.17.0",
"@javatypes/tomcat-websocket-api": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",

View File

@@ -46,6 +46,7 @@ export class WebSocket extends EventEmitter {
try {
let TransportImpl = require('./netty').NettyWebSocket
this.client = new TransportImpl(url, subProtocol, headers)
console.debug('create websocket from ' + this.client.constructor.name)
} catch (error) {
console.error('create websocket impl error: ' + error)
console.ex(error)
@@ -98,3 +99,4 @@ export class WebSocket extends EventEmitter {
this.removeAllListeners()
}
}
global.setGlobal('WebSocket', WebSocket)

View File

@@ -1,4 +1,3 @@
import { EventEmitter } from 'events'
import { NettyWebSocket } from '.'
import { WebSocketClientHandlerAdapter } from './adapter/handler'
@@ -6,6 +5,7 @@ const CharsetUtil = Java.type('io.netty.util.CharsetUtil')
const TextWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.TextWebSocketFrame')
const CloseWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.CloseWebSocketFrame')
const FullHttpResponse = Java.type('io.netty.handler.codec.http.FullHttpResponse')
const DefaultChannelPromise = Java.type('io.netty.channel.DefaultChannelPromise')
export class WebSocketClientHandler extends WebSocketClientHandlerAdapter {
public handshaker: any
@@ -20,16 +20,20 @@ export class WebSocketClientHandler extends WebSocketClientHandlerAdapter {
return true
}
handlerAdded(ctx: any) {
console.trace(`${ctx} handlerAdded`)
this.handshakeFuture = ctx.newPromise()
console.debug(`${ctx} handlerAdded`)
if (ctx.newPromise) {
this.handshakeFuture = ctx.newPromise()
} else {
this.handshakeFuture = new DefaultChannelPromise(ctx.channel(), ctx.executor())
}
}
channelActive(ctx: any) {
console.trace(`${ctx} channelActive`)
console.debug(`${ctx} channelActive`)
this.handshaker.handshake(ctx.channel())
}
channelInactive(ctx: any) {
console.trace(`${ctx} channelInactive`)
this.client.onclose({ code: 0, reason: 'server connection channel inactive!' })
console.debug(`${ctx} channelInactive`)
this.client.onclose({ code: 0, reason: 'client connection channel inactive!' })
}
channelRead0(ctx: any, msg: any) {
console.trace(`${ctx} channelRead0 ${msg}`)
@@ -50,11 +54,11 @@ export class WebSocketClientHandler extends WebSocketClientHandlerAdapter {
if (frame instanceof TextWebSocketFrame) {
this.client.onmessage({ data: frame.text() })
} else if (frame instanceof CloseWebSocketFrame) {
this.client.onclose({ code: 0, reason: 'server send CloseWebSocketFrame!' })
this.client.onclose({ code: 0, reason: 'server close connection!' })
}
}
exceptionCaught(ctx: any, cause: Error) {
console.trace(`${ctx} exceptionCaught ${cause}`)
console.debug(`${ctx} exceptionCaught ${cause}`)
this.client.onerror({ error: cause })
if (!this.handshakeFuture.isDone()) {
this.handshakeFuture.setFailure(cause)

View File

@@ -4,16 +4,12 @@ import { Transport } from '../transport'
import { WebSocketClientHandler } from './handler'
const URI = Java.type('java.net.URI')
const Epoll = Java.type('io.netty.channel.epoll.Epoll')
const Bootstrap = Java.type('io.netty.bootstrap.Bootstrap')
const ChannelFutureListener = Java.type('io.netty.channel.ChannelFutureListener')
const NioEventLoopGroup = Java.type('io.netty.channel.nio.NioEventLoopGroup')
const NioSocketChannel = Java.type('io.netty.channel.socket.nio.NioSocketChannel')
const EpollEventLoopGroup = Java.type('io.netty.channel.epoll.EpollEventLoopGroup')
const EpollSocketChannel = Java.type('io.netty.channel.epoll.EpollSocketChannel')
const WebSocketClientHandshakerFactory = Java.type('io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory')
const WebSocketVersion = Java.type('io.netty.handler.codec.http.websocketx.WebSocketVersion')
@@ -25,22 +21,77 @@ const CloseWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.Cl
const ChannelInitializer = Java.type('io.netty.channel.ChannelInitializer')
const DefaultHttpHeaders = Java.type('io.netty.handler.codec.http.DefaultHttpHeaders')
const epull = Epoll.isAvailable()
const group = epull ? new EpollEventLoopGroup() : new NioEventLoopGroup()
const socketChannelClass = epull ? EpollSocketChannel.class : NioSocketChannel.class
const AtomicInteger = Java.type("java.util.concurrent.atomic.AtomicInteger")
const channelCount = new AtomicInteger(0)
var SslContextBuilder: any
var InsecureTrustManagerFactory: any
var SSLContext: any
var SslHandler: any
try {
SslContextBuilder = Java.type('io.netty.handler.ssl.SslContextBuilder')
InsecureTrustManagerFactory = Java.type('io.netty.handler.ssl.util.InsecureTrustManagerFactory')
} catch (error) {
SSLContext = Java.type('javax.net.ssl.SSLContext')
SslHandler = Java.type('io.netty.handler.ssl.SslHandler')
}
var group: any
var socketChannelClass: any
try {
const Epoll = Java.type('io.netty.channel.epoll.Epoll')
const epull = Epoll.isAvailable()
const EpollEventLoopGroup = Java.type('io.netty.channel.epoll.EpollEventLoopGroup')
const EpollSocketChannel = Java.type('io.netty.channel.epoll.EpollSocketChannel')
group = epull ? new EpollEventLoopGroup() : new NioEventLoopGroup()
socketChannelClass = epull ? EpollSocketChannel.class : NioSocketChannel.class
} catch (error) {
group = new NioEventLoopGroup()
socketChannelClass = NioSocketChannel.class
}
process.on('exit', () => group.shutdownGracefully())
export class NettyWebSocket extends Transport {
private _uri: any
private _schema: string
private _host: string
private _port: number
private channel: any
private b = new Bootstrap();
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) {
super(url, subProtocol, headers)
if (!url) {
throw new Error("Failed to construct 'WebSocket': The URL '" + url + "' is invalid.")
}
this._uri = URI.create(this._url)
this._schema = this._uri.getScheme() ?? 'ws'
if (["wss", "ws"].indexOf(this._schema) == -1) {
throw new Error("Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. '" + this._schema + "' is not allowed.")
}
this._host = this._uri.getHost()
if (!this._host) {
throw new Error("Failed to construct 'WebSocket': The Host '" + this._host + "' is invalid.")
}
this._port = this._uri.getPort()
if (this._port == -1) {
if (this._schema == "wss") {
this._port = 443
} else if (this._schema == "ws") {
this._port = 80
}
}
console.debug(`constructor NettyWebSocket url: ${url} scheme: ${this._schema} host: ${this._host} port: ${this._port} header: ${JSON.stringify(headers)}`)
}
getId() {
return this.channel?.id() + ''
if (this.channel?.id) {
return this.channel?.id() + ''
}
return 'NettyWebSocket#' + channelCount.incrementAndGet()
}
doConnect() {
console.debug('client NettyWebSocket doConnect', this._url)
let uri = URI.create(this._url)
let headers = new DefaultHttpHeaders()
for (const key of Object.getOwnPropertyNames(this._headers || {})) {
@@ -56,20 +107,32 @@ export class NettyWebSocket extends Transport {
.handler(new ChannelInitializer({
initChannel: (ch: any) => {
let pipeline = ch.pipeline()
if (this._schema == "wss") {
if (SslContextBuilder) {
let sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build()
pipeline.addLast(sslCtx.newHandler(ch.alloc(), this._host, this._port))
} else {
let sslEngine = SSLContext.getDefault().createSSLEngine()
sslEngine.setUseClientMode(true)
pipeline.addLast("ssl", new SslHandler(sslEngine))
}
}
pipeline.addLast("http-codec", new HttpClientCodec())
pipeline.addLast("aggregator", new HttpObjectAggregator(65536))
pipeline.addLast("websocket", handler.getHandler())
}
}))
this.b.connect(uri.getHost(), uri.getPort()).addListener(new ChannelFutureListener((future: any) => {
this.b.connect(this._host, this._port).addListener(new ChannelFutureListener((future: any) => {
this.channel = future.sync().channel()
this.onconnection({})
handler.handshakeFuture.addListener(new ChannelFutureListener((future: any) => {
try {
future.sync()
// only trigger onconnect when not have error
this.onconnect({})
} catch (error) {
console.debug(error)
// ignore error exceptionCaught from handler
// this.onerror({ error })
}
}))
}))

View File

@@ -32,9 +32,11 @@ export abstract class Transport extends EventEmitter {
}
connect() {
console.debug(`client Transport connect`)
try {
this.doConnect()
} catch (error) {
console.ex(error)
this.onerror({ error })
}
}
@@ -55,6 +57,8 @@ export abstract class Transport extends EventEmitter {
this.doClose(code, reason)
} catch (error) {
this.onerror({ error })
} finally {
this.removeAllListeners()
}
} else {
console.debug(`${this.id} call close but state is ${this.readyStatus}`)
@@ -67,7 +71,6 @@ export abstract class Transport extends EventEmitter {
}
onconnect(event: Event) {
console.debug(`${this.id} call onconnect`)
if (this.readyStatus != WebSocket.OPEN) {
this.readyStatus = WebSocket.OPEN
this.emit('open', event)

View File

@@ -0,0 +1 @@
export = (namepsace) => (...args) => { }//console.debug(namepsace, ...args)

View File

@@ -0,0 +1,16 @@
import { Socket } from './socket'
export default (uri, opts) => new Socket(uri, opts)
/**
* Expose deps for legacy compatibility
* and standalone browser access.
*/
const protocol = Socket.protocol // this is an int
export { Socket, protocol }
// module.exports.Transport = require("./transport")
// module.exports.transports = require("./transports/index")
// module.exports.parser = require("../engine.io-parser")
export * from './transport'
export * from './transports/index'
export * from '../engine.io-parser'

View File

@@ -0,0 +1,688 @@
import transports from "./transports"
// const transports = require("./transports/index")
const Emitter = require("component-emitter")
const debug = (...args: any) => console.debug('engine.io-client:socket', ...args)//require("debug")("engine.io-client:socket")
import parser from "../engine.io-parser"
const parseuri = require("parseuri")
const parseqs = require("parseqs")
import { installTimerFunctions } from "./util"
export class Socket extends Emitter {
/**
* Socket constructor.
*
* @param {String|Object} uri or options
* @param {Object} options
* @api public
*/
constructor(uri, opts: any = {}) {
super()
if (uri && "object" === typeof uri) {
opts = uri
uri = null
}
if (uri) {
uri = parseuri(uri)
opts.hostname = uri.host
opts.secure = uri.protocol === "https" || uri.protocol === "wss"
opts.port = uri.port
if (uri.query) opts.query = uri.query
} else if (opts.host) {
opts.hostname = parseuri(opts.host).host
}
installTimerFunctions(this, opts)
this.secure =
null != opts.secure
? opts.secure
: typeof location !== "undefined" && "https:" === location.protocol
if (opts.hostname && !opts.port) {
// if no port is specified manually, use the protocol default
opts.port = this.secure ? "443" : "80"
}
this.hostname =
opts.hostname ||
(typeof location !== "undefined" ? location.hostname : "localhost")
this.port =
opts.port ||
(typeof location !== "undefined" && location.port
? location.port
: this.secure
? 443
: 80)
this.transports = ["websocket"]
this.readyState = ""
this.writeBuffer = []
this.prevBufferLen = 0
this.opts = Object.assign(
{
path: "/engine.io",
agent: false,
withCredentials: false,
upgrade: true,
jsonp: true,
timestampParam: "t",
rememberUpgrade: false,
rejectUnauthorized: true,
perMessageDeflate: {
threshold: 1024
},
transportOptions: {},
closeOnBeforeunload: true
},
opts
)
this.opts.path = this.opts.path.replace(/\/$/, "") + "/"
if (typeof this.opts.query === "string") {
this.opts.query = parseqs.decode(this.opts.query)
}
// set on handshake
this.id = null
this.upgrades = null
this.pingInterval = null
this.pingTimeout = null
// set on heartbeat
this.pingTimeoutTimer = null
if (typeof addEventListener === "function") {
if (this.opts.closeOnBeforeunload) {
// Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener
// ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is
// closed/reloaded)
addEventListener(
"beforeunload",
() => {
if (this.transport) {
// silently close the transport
this.transport.removeAllListeners()
this.transport.close()
}
},
false
)
}
if (this.hostname !== "localhost") {
this.offlineEventListener = () => {
this.onClose("transport close")
}
addEventListener("offline", this.offlineEventListener, false)
}
}
this.open()
}
/**
* Creates transport of the given type.
*
* @param {String} transport name
* @return {Transport}
* @api private
*/
createTransport(name, opt?) {
if (name != 'websocket') {
throw new Error('Only Support WebSocket in MiaoScript!')
}
debug('creating transport "%s"', name)
const query: any = clone(this.opts.query)
// append engine.io protocol identifier
query.EIO = parser.protocol
// transport name
query.transport = name
// session id if we already have one
if (this.id) query.sid = this.id
const opts = Object.assign(
{},
this.opts.transportOptions[name],
this.opts,
{
query,
socket: this,
hostname: this.hostname,
secure: this.secure,
port: this.port
}
)
debug("options: %j", JSON.stringify(opts))
debug("new func", transports[name])
return new transports[name](opts)
}
/**
* Initializes transport to use and starts probe.
*
* @api private
*/
open() {
let transport
if (
this.opts.rememberUpgrade &&
Socket.priorWebsocketSuccess &&
this.transports.indexOf("websocket") !== -1
) {
transport = "websocket"
} else if (0 === this.transports.length) {
// Emit error on next tick so it can be listened to
this.setTimeoutFn(() => {
this.emit("error", "No transports available")
}, 0)
return
} else {
transport = this.transports[0]
}
this.readyState = "opening"
// Retry with the next transport if the transport is disabled (jsonp: false)
try {
transport = this.createTransport(transport)
} catch (e) {
debug("error while creating transport: %s", e)
this.transports.shift()
this.open()
return
}
transport.open()
this.setTransport(transport)
}
/**
* Sets the current transport. Disables the existing one (if any).
*
* @api private
*/
setTransport(transport) {
debug("setting transport %s", transport.name)
if (this.transport) {
debug("clearing existing transport %s", this.transport.name)
this.transport.removeAllListeners()
}
// set up transport
this.transport = transport
// set up transport listeners
transport
.on("drain", this.onDrain.bind(this))
.on("packet", this.onPacket.bind(this))
.on("error", this.onError.bind(this))
.on("close", () => {
this.onClose("transport close")
})
}
/**
* Probes a transport.
*
* @param {String} transport name
* @api private
*/
probe(name) {
debug('probing transport "%s"', name)
let transport = this.createTransport(name, { probe: 1 })
let failed = false
Socket.priorWebsocketSuccess = false
const onTransportOpen = () => {
if (failed) return
debug('probe transport "%s" opened', name)
transport.send([{ type: "ping", data: "probe" }])
transport.once("packet", msg => {
if (failed) return
if ("pong" === msg.type && "probe" === msg.data) {
debug('probe transport "%s" pong', name)
this.upgrading = true
this.emit("upgrading", transport)
if (!transport) return
Socket.priorWebsocketSuccess = "websocket" === transport.name
debug('pausing current transport "%s"', this.transport.name)
this.transport.pause(() => {
if (failed) return
if ("closed" === this.readyState) return
debug("changing transport and sending upgrade packet")
cleanup()
this.setTransport(transport)
transport.send([{ type: "upgrade" }])
this.emit("upgrade", transport)
transport = null
this.upgrading = false
this.flush()
})
} else {
debug('probe transport "%s" failed', name)
const err: any = new Error("probe error")
err.transport = transport.name
this.emit("upgradeError", err)
}
})
}
function freezeTransport() {
if (failed) return
// Any callback called by transport should be ignored since now
failed = true
cleanup()
transport.close()
transport = null
}
// Handle any error that happens while probing
const onerror = err => {
const error: any = new Error("probe error: " + err)
error.transport = transport.name
freezeTransport()
debug('probe transport "%s" failed because of error: %s', name, err)
this.emit("upgradeError", error)
}
function onTransportClose() {
onerror("transport closed")
}
// When the socket is closed while we're probing
function onclose() {
onerror("socket closed")
}
// When the socket is upgraded while we're probing
function onupgrade(to) {
if (transport && to.name !== transport.name) {
debug('"%s" works - aborting "%s"', to.name, transport.name)
freezeTransport()
}
}
// Remove all listeners on the transport and on self
const cleanup = () => {
transport.removeListener("open", onTransportOpen)
transport.removeListener("error", onerror)
transport.removeListener("close", onTransportClose)
this.removeListener("close", onclose)
this.removeListener("upgrading", onupgrade)
}
transport.once("open", onTransportOpen)
transport.once("error", onerror)
transport.once("close", onTransportClose)
this.once("close", onclose)
this.once("upgrading", onupgrade)
transport.open()
}
/**
* Called when connection is deemed open.
*
* @api public
*/
onOpen() {
debug("socket open")
this.readyState = "open"
Socket.priorWebsocketSuccess = "websocket" === this.transport.name
this.emit("open")
this.flush()
// we check for `readyState` in case an `open`
// listener already closed the socket
if (
"open" === this.readyState &&
this.opts.upgrade &&
this.transport.pause
) {
debug("starting upgrade probes")
let i = 0
const l = this.upgrades.length
for (; i < l; i++) {
this.probe(this.upgrades[i])
}
}
}
/**
* Handles a packet.
*
* @api private
*/
onPacket(packet) {
if (
"opening" === this.readyState ||
"open" === this.readyState ||
"closing" === this.readyState
) {
debug('socket receive: type "%s", data "%s"', packet.type, packet.data)
this.emit("packet", packet)
// Socket is live - any packet counts
this.emit("heartbeat")
switch (packet.type) {
case "open":
this.onHandshake(JSON.parse(packet.data))
break
case "ping":
this.resetPingTimeout()
this.sendPacket("pong")
this.emit("ping")
this.emit("pong")
break
case "error":
const err: any = new Error("server error")
err.code = packet.data
this.onError(err)
break
case "message":
this.emit("data", packet.data)
this.emit("message", packet.data)
break
}
} else {
debug('packet received with socket readyState "%s"', this.readyState)
}
}
/**
* Called upon handshake completion.
*
* @param {Object} handshake obj
* @api private
*/
onHandshake(data) {
this.emit("handshake", data)
this.id = data.sid
this.transport.query.sid = data.sid
this.upgrades = this.filterUpgrades(data.upgrades)
this.pingInterval = data.pingInterval
this.pingTimeout = data.pingTimeout
this.onOpen()
// In case open handler closes socket
if ("closed" === this.readyState) return
this.resetPingTimeout()
}
/**
* Sets and resets ping timeout timer based on server pings.
*
* @api private
*/
resetPingTimeout() {
this.clearTimeoutFn(this.pingTimeoutTimer)
this.pingTimeoutTimer = this.setTimeoutFn(() => {
this.onClose("ping timeout")
}, this.pingInterval + this.pingTimeout)
if (this.opts.autoUnref) {
this.pingTimeoutTimer.unref()
}
}
/**
* Called on `drain` event
*
* @api private
*/
onDrain() {
this.writeBuffer.splice(0, this.prevBufferLen)
// setting prevBufferLen = 0 is very important
// for example, when upgrading, upgrade packet is sent over,
// and a nonzero prevBufferLen could cause problems on `drain`
this.prevBufferLen = 0
if (0 === this.writeBuffer.length) {
this.emit("drain")
} else {
this.flush()
}
}
/**
* Flush write buffers.
*
* @api private
*/
flush() {
if (
"closed" !== this.readyState &&
this.transport.writable &&
!this.upgrading &&
this.writeBuffer.length
) {
debug("flushing %d packets in socket", this.writeBuffer.length)
this.transport.send(this.writeBuffer)
// keep track of current length of writeBuffer
// splice writeBuffer and callbackBuffer on `drain`
this.prevBufferLen = this.writeBuffer.length
this.emit("flush")
}
}
/**
* Sends a message.
*
* @param {String} message.
* @param {Function} callback function.
* @param {Object} options.
* @return {Socket} for chaining.
* @api public
*/
write(msg, options, fn) {
this.sendPacket("message", msg, options, fn)
return this
}
send(msg, options, fn) {
this.sendPacket("message", msg, options, fn)
return this
}
/**
* Sends a packet.
*
* @param {String} packet type.
* @param {String} data.
* @param {Object} options.
* @param {Function} callback function.
* @api private
*/
sendPacket(type, data?, options?, fn?) {
if ("function" === typeof data) {
fn = data
data = undefined
}
if ("function" === typeof options) {
fn = options
options = null
}
if ("closing" === this.readyState || "closed" === this.readyState) {
return
}
options = options || {}
options.compress = false !== options.compress
const packet = {
type: type,
data: data,
options: options
}
this.emit("packetCreate", packet)
this.writeBuffer.push(packet)
if (fn) this.once("flush", fn)
this.flush()
}
/**
* Closes the connection.
*
* @api private
*/
close() {
const close = () => {
this.onClose("forced close")
debug("socket closing - telling transport to close")
this.transport.close()
}
const cleanupAndClose = () => {
this.removeListener("upgrade", cleanupAndClose)
this.removeListener("upgradeError", cleanupAndClose)
close()
}
const waitForUpgrade = () => {
// wait for upgrade to finish since we can't send packets while pausing a transport
this.once("upgrade", cleanupAndClose)
this.once("upgradeError", cleanupAndClose)
}
if ("opening" === this.readyState || "open" === this.readyState) {
this.readyState = "closing"
if (this.writeBuffer.length) {
this.once("drain", () => {
if (this.upgrading) {
waitForUpgrade()
} else {
close()
}
})
} else if (this.upgrading) {
waitForUpgrade()
} else {
close()
}
}
return this
}
/**
* Called upon transport error
*
* @api private
*/
onError(err) {
debug("socket error %j", err)
Socket.priorWebsocketSuccess = false
this.emit("error", err)
this.onClose("transport error", err)
}
/**
* Called upon transport close.
*
* @api private
*/
onClose(reason, desc?) {
if (
"opening" === this.readyState ||
"open" === this.readyState ||
"closing" === this.readyState
) {
debug('socket close with reason: "%s"', reason)
// clear timers
this.clearTimeoutFn(this.pingIntervalTimer)
this.clearTimeoutFn(this.pingTimeoutTimer)
// stop event from firing again for transport
this.transport.removeAllListeners("close")
// ensure transport won't stay open
this.transport.close()
// ignore further transport communication
this.transport.removeAllListeners()
if (typeof removeEventListener === "function") {
removeEventListener("offline", this.offlineEventListener, false)
}
// set ready state
this.readyState = "closed"
// clear session id
this.id = null
// emit close event
this.emit("close", reason, desc)
// clean buffers after, so users can still
// grab the buffers on `close` event
this.writeBuffer = []
this.prevBufferLen = 0
}
}
/**
* Filters upgrades, returning only those matching client transports.
*
* @param {Array} server upgrades
* @api private
*
*/
filterUpgrades(upgrades) {
const filteredUpgrades = []
let i = 0
const j = upgrades.length
for (; i < j; i++) {
if (~this.transports.indexOf(upgrades[i]))
filteredUpgrades.push(upgrades[i])
}
return filteredUpgrades
}
}
Socket.priorWebsocketSuccess = false
/**
* Protocol version.
*
* @api public
*/
Socket.protocol = parser.protocol // this is an int
function clone(obj) {
const o = {}
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = obj[i]
}
}
return o
}

View File

@@ -0,0 +1,119 @@
import parser from "../engine.io-parser"
const Emitter = require("component-emitter")
import { installTimerFunctions } from "./util"
const debug = (...args: any) => console.debug('engine.io-client:transport', ...args)//require("debug")("engine.io-client:transport")
export class Transport extends Emitter {
/**
* Transport abstract constructor.
*
* @param {Object} options.
* @api private
*/
constructor(opts) {
super()
installTimerFunctions(this, opts)
this.opts = opts
this.query = opts.query
this.readyState = ""
this.socket = opts.socket
}
/**
* Emits an error.
*
* @param {String} str
* @return {Transport} for chaining
* @api public
*/
onError(msg, desc) {
const err: any = new Error(msg)
err.type = "TransportError"
err.description = desc
this.emit("error", err)
return this
}
/**
* Opens the transport.
*
* @api public
*/
open() {
if ("closed" === this.readyState || "" === this.readyState) {
this.readyState = "opening"
this.doOpen()
}
return this
}
/**
* Closes the transport.
*
* @api private
*/
close() {
if ("opening" === this.readyState || "open" === this.readyState) {
this.doClose()
this.onClose()
}
return this
}
/**
* Sends multiple packets.
*
* @param {Array} packets
* @api private
*/
send(packets) {
if ("open" === this.readyState) {
this.write(packets)
} else {
// this might happen if the transport was silently closed in the beforeunload event handler
debug("transport is not open, discarding packets")
}
}
/**
* Called upon open
*
* @api private
*/
onOpen() {
this.readyState = "open"
this.writable = true
this.emit("open")
}
/**
* Called with data.
*
* @param {String} data
* @api private
*/
onData(data) {
const packet = parser.decodePacket(data, this.socket.binaryType)
this.onPacket(packet)
}
/**
* Called with a decoded packet.
*/
onPacket(packet) {
this.emit("packet", packet)
}
/**
* Called upon close.
*
* @api private
*/
onClose() {
this.readyState = "closed"
this.emit("close")
}
}

View File

@@ -0,0 +1,4 @@
import { WS } from "./websocket"
export default {
'websocket': WS
}

View File

@@ -0,0 +1,259 @@
import { Transport } from '../transport'
// const Transport = require("../transport")
import parser from '../../engine.io-parser'
// const parser = require("../engine.io-parser")
const parseqs = require("parseqs")
const yeast = require("yeast")
import { pick } from '../util'
// const { pick } = require("../util")
import { WebSocket } from '../../client'
const usingBrowserWebSocket = true
// const {
// WebSocket,
// usingBrowserWebSocket,
// defaultBinaryType,
// nextTick
// } = require("./websocket-constructor")
const debug = (...args: any) => console.debug('engine.io-client:websocket', ...args)//require("debug")("engine.io-client:websocket")
// detect ReactNative environment
const isReactNative =
typeof navigator !== "undefined" &&
typeof navigator.product === "string" &&
navigator.product.toLowerCase() === "reactnative"
export class WS extends Transport {
/**
* WebSocket transport constructor.
*
* @api {Object} connection options
* @api public
*/
constructor(opts) {
super(opts)
this.supportsBinary = !opts.forceBase64
}
/**
* Transport name.
*
* @api public
*/
get name() {
return "websocket"
}
/**
* Opens socket.
*
* @api private
*/
doOpen() {
if (!this.check()) {
// let probe timeout
return
}
const uri = this.uri()
const protocols = this.opts.protocols
// React Native only supports the 'headers' option, and will print a warning if anything else is passed
const opts = isReactNative
? {}
: pick(
this.opts,
"agent",
"perMessageDeflate",
"pfx",
"key",
"passphrase",
"cert",
"ca",
"ciphers",
"rejectUnauthorized",
"localAddress",
"protocolVersion",
"origin",
"maxPayload",
"family",
"checkServerIdentity"
)
if (this.opts.extraHeaders) {
opts.headers = this.opts.extraHeaders
}
try {
this.ws = new WebSocket(uri, protocols)
// usingBrowserWebSocket && !isReactNative
// ? protocols
// ? new WebSocket(uri, protocols)
// : new WebSocket(uri)
// : new WebSocket(uri, protocols, opts)
} catch (err) {
return this.emit("error", err)
}
this.ws.binaryType = this.socket.binaryType || 'arraybuffer'
this.addEventListeners()
}
/**
* Adds event listeners to the socket
*
* @api private
*/
addEventListeners() {
this.ws.onopen = () => {
if (this.opts.autoUnref) {
this.ws._socket.unref()
}
this.onOpen()
}
this.ws.onclose = this.onClose.bind(this)
this.ws.onmessage = ev => this.onData(ev.data)
this.ws.onerror = e => this.onError("websocket error", e)
}
/**
* Writes data to socket.
*
* @param {Array} array of packets.
* @api private
*/
write(packets) {
this.writable = false
// encodePacket efficient as it uses WS framing
// no need for encodePayload
for (let i = 0; i < packets.length; i++) {
const packet = packets[i]
const lastPacket = i === packets.length - 1
parser.encodePacket(packet, this.supportsBinary, data => {
// always create a new object (GH-437)
const opts: any = {}
if (!usingBrowserWebSocket) {
if (packet.options) {
opts.compress = packet.options.compress
}
if (this.opts.perMessageDeflate) {
const len =
"string" === typeof data ? Buffer.byteLength(data) : data.length
if (len < this.opts.perMessageDeflate.threshold) {
opts.compress = false
}
}
}
// Sometimes the websocket has already been closed but the browser didn't
// have a chance of informing us about it yet, in that case send will
// throw an error
try {
if (usingBrowserWebSocket) {
// TypeError is thrown when passing the second argument on Safari
this.ws.send(data)
} else {
this.ws.send(data, opts)
}
} catch (e) {
debug("websocket closed before onclose event")
}
if (lastPacket) {
// fake drain
// defer to next tick to allow Socket to clear writeBuffer
process.nextTick(() => {
this.writable = true
this.emit("drain")
}, this.setTimeoutFn)
}
})
}
}
/**
* Called upon close
*
* @api private
*/
onClose() {
Transport.prototype.onClose.call(this)
}
/**
* Closes socket.
*
* @api private
*/
doClose() {
if (typeof this.ws !== "undefined") {
this.ws.close()
this.ws = null
}
}
/**
* Generates uri for connection.
*
* @api private
*/
uri() {
let query = this.query || {}
const schema = this.opts.secure ? "wss" : "ws"
let port = ""
// avoid port if default for schema
if (
this.opts.port &&
(("wss" === schema && Number(this.opts.port) !== 443) ||
("ws" === schema && Number(this.opts.port) !== 80))
) {
port = ":" + this.opts.port
}
// append timestamp to URI
if (this.opts.timestampRequests) {
query[this.opts.timestampParam] = yeast()
}
// communicate binary support capabilities
if (!this.supportsBinary) {
query.b64 = 1
}
query = parseqs.encode(query)
// prepend ? to query
if (query.length) {
query = "?" + query
}
const ipv6 = this.opts.hostname.indexOf(":") !== -1
return (
schema +
"://" +
(ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) +
port +
this.opts.path +
query
)
}
/**
* Feature detection for WebSocket.
*
* @return {Boolean} whether this transport is available.
* @api public
*/
check() {
return (
!!WebSocket &&
!("__initialize" in WebSocket && this.name === WS.prototype.name)
)
}
}

View File

@@ -0,0 +1,23 @@
const pick = (obj, ...attr) => {
return attr.reduce((acc, k) => {
if (obj.hasOwnProperty(k)) {
acc[k] = obj[k]
}
return acc
}, {})
}
// Keep a reference to the real timeout functions so they can be used when overridden
const NATIVE_SET_TIMEOUT = setTimeout
const NATIVE_CLEAR_TIMEOUT = clearTimeout
const installTimerFunctions = (obj, opts) => {
if (opts.useNativeTimers) {
obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis)
obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis)
} else {
obj.setTimeoutFn = setTimeout.bind(globalThis)
obj.clearTimeoutFn = clearTimeout.bind(globalThis)
}
}
export { pick, installTimerFunctions }

View File

@@ -48,3 +48,4 @@ export * from './socket.io'
export * from './client'
export * from './server'
export * from './engine.io/transport'
export * as client from './socket.io-client'

View File

@@ -49,9 +49,6 @@ export abstract class WebSocketServer extends EventEmitter {
this.execute(handler, (websocket) => websocket.emit(ServerEvent.disconnect, cause))
}
protected onerror(handler: any, error: Error) {
if (global.debug) {
console.ex(error)
}
this.execute(handler, (websocket) => websocket.emit(ServerEvent.error, error))
}
protected execute(handler: any, callback: (websocket: WebSocketClient) => void) {
@@ -82,9 +79,9 @@ export const attach = (instance, options) => {
}, options)
let WebSocketServerImpl = undefined
if (instance.class.name.startsWith('io.netty.channel')) {
WebSocketServerImpl = require("../netty").NettyWebSocketServer
WebSocketServerImpl = require("./netty").NettyWebSocketServer
} else {
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
WebSocketServerImpl = require("./tomcat").TomcatWebSocketServer
}
return new WebSocketServerImpl(instance, options)
}

View File

@@ -1,4 +1,4 @@
import { WebSocketClient } from '../server/client'
import { WebSocketClient } from '../client'
const TextWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.TextWebSocketFrame')

View File

@@ -1,8 +1,8 @@
import { JavaServerOptions } from '../server'
import { HttpRequestHandlerAdapter } from './adapter'
import { AttributeKeys } from './constants'
import type { JavaServerOptions } from '../'
const DefaultHttpResponse = Java.type('io.netty.handler.codec.http.DefaultHttpResponse')
const DefaultFullHttpResponse = Java.type('io.netty.handler.codec.http.DefaultFullHttpResponse')
const HttpHeaders = Java.type('io.netty.handler.codec.http.HttpHeaders')

View File

@@ -1,11 +1,13 @@
import { JavaServerOptions, ServerEvent, WebSocketServer } from '../server'
import { Request } from '../server/request'
import { ServerEvent, WebSocketServer } from '../'
import { Request } from '../request'
import { NettyClient } from './client'
import { AttributeKeys, Keys } from './constants'
import { WebSocketDetect } from './websocket_detect'
import { WebSocketHandler } from './websocket_handler'
import type { JavaServerOptions } from '../'
class NettyWebSocketServer extends WebSocketServer {
constructor(pipeline: any, options: JavaServerOptions) {
super(pipeline, options)

View File

@@ -1,8 +1,10 @@
import { EventEmitter } from 'events'
import { JavaServerOptions, ServerEvent } from '../server'
import { ServerEvent } from '../'
import { TextWebSocketFrameHandlerAdapter } from './adapter'
import type { JavaServerOptions } from '../'
export class TextWebSocketFrameHandler extends TextWebSocketFrameHandlerAdapter {
private event: EventEmitter
constructor(options: JavaServerOptions) {

View File

@@ -1,7 +1,7 @@
import { EventEmitter } from 'events'
import { WebSocketHandlerAdapter } from "./adapter"
import { ServerEvent } from '../server'
import { ServerEvent } from '../'
export class WebSocketDetect extends WebSocketHandlerAdapter {
private event: EventEmitter

View File

@@ -1,10 +1,12 @@
import { JavaServerOptions, ServerEvent } from '../server'
import { ServerEvent } from '../'
import { Keys } from './constants'
import { HttpRequestHandler } from './httprequest'
import { WebSocketHandlerAdapter } from "./adapter"
import { TextWebSocketFrameHandler } from './text_websocket_frame'
import type { JavaServerOptions } from '../'
const CharsetUtil = Java.type('io.netty.util.CharsetUtil')
const HttpServerCodec = Java.type('io.netty.handler.codec.http.HttpServerCodec')
const ChunkedWriteHandler = Java.type('io.netty.handler.stream.ChunkedWriteHandler')

View File

@@ -1,4 +1,4 @@
import { WebSocketClient } from '../server/client'
import { WebSocketClient } from '../client'
export class TomcatClient extends WebSocketClient {
private session: javax.websocket.Session

View File

@@ -1,5 +1,5 @@
import { JavaServerOptions, WebSocketServer } from '../server'
import { Request } from '../server/request'
import { JavaServerOptions, WebSocketServer } from '../'
import { Request } from '../request'
import { TomcatClient } from './client'
import { ProxyBeanName } from './constants'

View File

@@ -0,0 +1,105 @@
import { url } from "./url"
import { Manager, ManagerOptions } from "./manager"
import { Socket, SocketOptions } from "./socket"
const debug = require("../debug")("socket.io-client")
/**
* Module exports.
*/
module.exports = exports = lookup
/**
* Managers cache.
*/
const cache: Record<string, Manager> = (exports.managers = {})
/**
* Looks up an existing `Manager` for multiplexing.
* If the user summons:
*
* `io('http://localhost/a');`
* `io('http://localhost/b');`
*
* We reuse the existing instance based on same scheme/port/host,
* and we initialize sockets for each namespace.
*
* @public
*/
function lookup(opts?: Partial<ManagerOptions & SocketOptions>): Socket
function lookup(
uri: string,
opts?: Partial<ManagerOptions & SocketOptions>
): Socket
function lookup(
uri: string | Partial<ManagerOptions & SocketOptions>,
opts?: Partial<ManagerOptions & SocketOptions>
): Socket
function lookup(
uri: string | Partial<ManagerOptions & SocketOptions>,
opts?: Partial<ManagerOptions & SocketOptions>
): Socket {
if (typeof uri === "object") {
opts = uri
uri = undefined
}
opts = opts || {}
const parsed = url(uri as string, opts.path || "/socket.io")
const source = parsed.source
const id = parsed.id
const path = parsed.path
const sameNamespace = cache[id] && path in cache[id]["nsps"]
const newConnection =
opts.forceNew ||
opts["force new connection"] ||
false === opts.multiplex ||
sameNamespace
let io: Manager
if (newConnection) {
debug("ignoring socket cache for %s", source)
io = new Manager(source, opts)
} else {
if (!cache[id]) {
debug("new io instance for %s", source)
cache[id] = new Manager(source, opts)
}
io = cache[id]
}
if (parsed.query && !opts.query) {
opts.query = parsed.queryKey
}
return io.socket(parsed.path, opts)
}
/**
* Protocol version.
*
* @public
*/
export { protocol } from "../socket.io-parser"
/**
* `connect`.
*
* @param {String} uri
* @public
*/
exports.connect = lookup
/**
* Expose constructors for standalone build.
*
* @public
*/
export { Manager, ManagerOptions } from "./manager"
export { Socket } from "./socket"
export { lookup as io, SocketOptions }
export default lookup

View File

@@ -0,0 +1,816 @@
import eio from "../engine.io-client"
import { Socket, SocketOptions } from "./socket"
import * as parser from "../socket.io-parser"
import { Decoder, Encoder, Packet } from "../socket.io-parser"
import { on } from "./on"
import * as Backoff from "backo2"
import {
DefaultEventsMap,
EventsMap,
StrictEventEmitter,
} from "./typed-events"
const debug = require("../debug")("socket.io-client")
interface EngineOptions {
/**
* The host that we're connecting to. Set from the URI passed when connecting
*/
host: string
/**
* The hostname for our connection. Set from the URI passed when connecting
*/
hostname: string
/**
* If this is a secure connection. Set from the URI passed when connecting
*/
secure: boolean
/**
* The port for our connection. Set from the URI passed when connecting
*/
port: string
/**
* Any query parameters in our uri. Set from the URI passed when connecting
*/
query: { [key: string]: string }
/**
* `http.Agent` to use, defaults to `false` (NodeJS only)
*/
agent: string | boolean
/**
* Whether the client should try to upgrade the transport from
* long-polling to something better.
* @default true
*/
upgrade: boolean
/**
* Forces JSONP for polling transport.
*/
forceJSONP: boolean
/**
* Determines whether to use JSONP when necessary for polling. If
* disabled (by settings to false) an error will be emitted (saying
* "No transports available") if no other transports are available.
* If another transport is available for opening a connection (e.g.
* WebSocket) that transport will be used instead.
* @default true
*/
jsonp: boolean
/**
* Forces base 64 encoding for polling transport even when XHR2
* responseType is available and WebSocket even if the used standard
* supports binary.
*/
forceBase64: boolean
/**
* Enables XDomainRequest for IE8 to avoid loading bar flashing with
* click sound. default to `false` because XDomainRequest has a flaw
* of not sending cookie.
* @default false
*/
enablesXDR: boolean
/**
* The param name to use as our timestamp key
* @default 't'
*/
timestampParam: string
/**
* Whether to add the timestamp with each transport request. Note: this
* is ignored if the browser is IE or Android, in which case requests
* are always stamped
* @default false
*/
timestampRequests: boolean
/**
* A list of transports to try (in order). Engine.io always attempts to
* connect directly with the first one, provided the feature detection test
* for it passes.
* @default ['polling','websocket']
*/
transports: string[]
/**
* The port the policy server listens on
* @default 843
*/
policyPost: number
/**
* If true and if the previous websocket connection to the server succeeded,
* the connection attempt will bypass the normal upgrade process and will
* initially try websocket. A connection attempt following a transport error
* will use the normal upgrade process. It is recommended you turn this on
* only when using SSL/TLS connections, or if you know that your network does
* not block websockets.
* @default false
*/
rememberUpgrade: boolean
/**
* Are we only interested in transports that support binary?
*/
onlyBinaryUpgrades: boolean
/**
* Timeout for xhr-polling requests in milliseconds (0) (only for polling transport)
*/
requestTimeout: number
/**
* Transport options for Node.js client (headers etc)
*/
transportOptions: Object
/**
* (SSL) Certificate, Private key and CA certificates to use for SSL.
* Can be used in Node.js client environment to manually specify
* certificate information.
*/
pfx: string
/**
* (SSL) Private key to use for SSL. Can be used in Node.js client
* environment to manually specify certificate information.
*/
key: string
/**
* (SSL) A string or passphrase for the private key or pfx. Can be
* used in Node.js client environment to manually specify certificate
* information.
*/
passphrase: string
/**
* (SSL) Public x509 certificate to use. Can be used in Node.js client
* environment to manually specify certificate information.
*/
cert: string
/**
* (SSL) An authority certificate or array of authority certificates to
* check the remote host against.. Can be used in Node.js client
* environment to manually specify certificate information.
*/
ca: string | string[]
/**
* (SSL) A string describing the ciphers to use or exclude. Consult the
* [cipher format list]
* (http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT) for
* details on the format.. Can be used in Node.js client environment to
* manually specify certificate information.
*/
ciphers: string
/**
* (SSL) If true, the server certificate is verified against the list of
* supplied CAs. An 'error' event is emitted if verification fails.
* Verification happens at the connection level, before the HTTP request
* is sent. Can be used in Node.js client environment to manually specify
* certificate information.
*/
rejectUnauthorized: boolean
/**
* Headers that will be passed for each request to the server (via xhr-polling and via websockets).
* These values then can be used during handshake or for special proxies.
*/
extraHeaders?: { [header: string]: string }
/**
* Whether to include credentials (cookies, authorization headers, TLS
* client certificates, etc.) with cross-origin XHR polling requests
* @default false
*/
withCredentials: boolean
/**
* Whether to automatically close the connection whenever the beforeunload event is received.
* @default true
*/
closeOnBeforeunload: boolean
}
export interface ManagerOptions extends EngineOptions {
/**
* Should we force a new Manager for this connection?
* @default false
*/
forceNew: boolean
/**
* Should we multiplex our connection (reuse existing Manager) ?
* @default true
*/
multiplex: boolean
/**
* The path to get our client file from, in the case of the server
* serving it
* @default '/socket.io'
*/
path: string
/**
* Should we allow reconnections?
* @default true
*/
reconnection: boolean
/**
* How many reconnection attempts should we try?
* @default Infinity
*/
reconnectionAttempts: number
/**
* The time delay in milliseconds between reconnection attempts
* @default 1000
*/
reconnectionDelay: number
/**
* The max time delay in milliseconds between reconnection attempts
* @default 5000
*/
reconnectionDelayMax: number
/**
* Used in the exponential backoff jitter when reconnecting
* @default 0.5
*/
randomizationFactor: number
/**
* The timeout in milliseconds for our connection attempt
* @default 20000
*/
timeout: number
/**
* Should we automatically connect?
* @default true
*/
autoConnect: boolean
/**
* weather we should unref the reconnect timer when it is
* create automatically
* @default false
*/
autoUnref: boolean
/**
* the parser to use. Defaults to an instance of the Parser that ships with socket.io.
*/
parser: any
}
interface ManagerReservedEvents {
open: () => void
error: (err: Error) => void
ping: () => void
packet: (packet: Packet) => void
close: (reason: string) => void
reconnect_failed: () => void
reconnect_attempt: (attempt: number) => void
reconnect_error: (err: Error) => void
reconnect: (attempt: number) => void
}
export class Manager<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<{}, {}, ManagerReservedEvents> {
/**
* The Engine.IO client instance
*
* @public
*/
public engine: any
/**
* @private
*/
_autoConnect: boolean
/**
* @private
*/
_readyState: "opening" | "open" | "closed"
/**
* @private
*/
_reconnecting: boolean
private readonly uri: string
public opts: Partial<ManagerOptions>
private nsps: Record<string, Socket> = {};
private subs: Array<ReturnType<typeof on>> = [];
private backoff: Backoff
private _reconnection: boolean
private _reconnectionAttempts: number
private _reconnectionDelay: number
private _randomizationFactor: number
private _reconnectionDelayMax: number
private _timeout: any
private encoder: Encoder
private decoder: Decoder
private skipReconnect: boolean
/**
* `Manager` constructor.
*
* @param uri - engine instance or engine uri/opts
* @param opts - options
* @public
*/
constructor(opts: Partial<ManagerOptions>)
constructor(uri?: string, opts?: Partial<ManagerOptions>)
constructor(
uri?: string | Partial<ManagerOptions>,
opts?: Partial<ManagerOptions>
)
constructor(
uri?: string | Partial<ManagerOptions>,
opts?: Partial<ManagerOptions>
) {
super()
if (uri && "object" === typeof uri) {
opts = uri
uri = undefined
}
opts = opts || {}
opts.path = opts.path || "/socket.io"
this.opts = opts
this.reconnection(opts.reconnection !== false)
this.reconnectionAttempts(opts.reconnectionAttempts || Infinity)
this.reconnectionDelay(opts.reconnectionDelay || 1000)
this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000)
this.randomizationFactor(opts.randomizationFactor ?? 0.5)
this.backoff = new Backoff({
min: this.reconnectionDelay(),
max: this.reconnectionDelayMax(),
jitter: this.randomizationFactor(),
})
this.timeout(null == opts.timeout ? 20000 : opts.timeout)
this._readyState = "closed"
this.uri = uri as string
const _parser = opts.parser || parser
this.encoder = new _parser.Encoder()
this.decoder = new _parser.Decoder()
this._autoConnect = opts.autoConnect !== false
if (this._autoConnect) this.open()
}
/**
* Sets the `reconnection` config.
*
* @param {Boolean} v - true/false if it should automatically reconnect
* @return {Manager} self or value
* @public
*/
public reconnection(v: boolean): this
public reconnection(): boolean
public reconnection(v?: boolean): this | boolean
public reconnection(v?: boolean): this | boolean {
if (!arguments.length) return this._reconnection
this._reconnection = !!v
return this
}
/**
* Sets the reconnection attempts config.
*
* @param {Number} v - max reconnection attempts before giving up
* @return {Manager} self or value
* @public
*/
public reconnectionAttempts(v: number): this
public reconnectionAttempts(): number
public reconnectionAttempts(v?: number): this | number
public reconnectionAttempts(v?: number): this | number {
if (v === undefined) return this._reconnectionAttempts
this._reconnectionAttempts = v
return this
}
/**
* Sets the delay between reconnections.
*
* @param {Number} v - delay
* @return {Manager} self or value
* @public
*/
public reconnectionDelay(v: number): this
public reconnectionDelay(): number
public reconnectionDelay(v?: number): this | number
public reconnectionDelay(v?: number): this | number {
if (v === undefined) return this._reconnectionDelay
this._reconnectionDelay = v
this.backoff?.setMin(v)
return this
}
/**
* Sets the randomization factor
*
* @param v - the randomization factor
* @return self or value
* @public
*/
public randomizationFactor(v: number): this
public randomizationFactor(): number
public randomizationFactor(v?: number): this | number
public randomizationFactor(v?: number): this | number {
if (v === undefined) return this._randomizationFactor
this._randomizationFactor = v
this.backoff?.setJitter(v)
return this
}
/**
* Sets the maximum delay between reconnections.
*
* @param v - delay
* @return self or value
* @public
*/
public reconnectionDelayMax(v: number): this
public reconnectionDelayMax(): number
public reconnectionDelayMax(v?: number): this | number
public reconnectionDelayMax(v?: number): this | number {
if (v === undefined) return this._reconnectionDelayMax
this._reconnectionDelayMax = v
this.backoff?.setMax(v)
return this
}
/**
* Sets the connection timeout. `false` to disable
*
* @param v - connection timeout
* @return self or value
* @public
*/
public timeout(v: number | boolean): this
public timeout(): number | boolean
public timeout(v?: number | boolean): this | number | boolean
public timeout(v?: number | boolean): this | number | boolean {
if (!arguments.length) return this._timeout
this._timeout = v
return this
}
/**
* Starts trying to reconnect if reconnection is enabled and we have not
* started reconnecting yet
*
* @private
*/
private maybeReconnectOnOpen() {
// Only try to reconnect if it's the first time we're connecting
if (
!this._reconnecting &&
this._reconnection &&
this.backoff.attempts === 0
) {
// keeps reconnection from firing twice for the same reconnection loop
this.reconnect()
}
}
/**
* Sets the current transport `socket`.
*
* @param {Function} fn - optional, callback
* @return self
* @public
*/
public open(fn?: (err?: Error) => void): this {
debug("readyState %s", this._readyState)
if (~this._readyState.indexOf("open")) return this
debug("opening %s", this.uri)
// @ts-ignore
this.engine = eio(this.uri, this.opts)
const socket = this.engine
const self = this
this._readyState = "opening"
this.skipReconnect = false
// emit `open`
const openSubDestroy = on(socket, "open", function () {
self.onopen()
fn && fn()
})
// emit `error`
const errorSub = on(socket, "error", (err) => {
debug("error")
self.cleanup()
self._readyState = "closed"
this.emitReserved("error", err)
if (fn) {
fn(err)
} else {
// Only do this if there is no fn to handle the error
self.maybeReconnectOnOpen()
}
})
if (false !== this._timeout) {
const timeout = this._timeout
debug("connect attempt will timeout after %d", timeout)
if (timeout === 0) {
openSubDestroy() // prevents a race condition with the 'open' event
}
// set timer
const timer = setTimeout(() => {
debug("connect attempt timed out after %d", timeout)
openSubDestroy()
socket.close()
socket.emit("error", new Error("timeout"))
}, timeout)
if (this.opts.autoUnref) {
timer.unref()
}
this.subs.push(function subDestroy(): void {
clearTimeout(timer)
})
}
this.subs.push(openSubDestroy)
this.subs.push(errorSub)
return this
}
/**
* Alias for open()
*
* @return self
* @public
*/
public connect(fn?: (err?: Error) => void): this {
return this.open(fn)
}
/**
* Called upon transport open.
*
* @private
*/
private onopen(): void {
debug("open")
// clear old subs
this.cleanup()
// mark as open
this._readyState = "open"
this.emitReserved("open")
// add new subs
const socket = this.engine
this.subs.push(
on(socket, "ping", this.onping.bind(this)),
on(socket, "data", this.ondata.bind(this)),
on(socket, "error", this.onerror.bind(this)),
on(socket, "close", this.onclose.bind(this)),
on(this.decoder, "decoded", this.ondecoded.bind(this))
)
}
/**
* Called upon a ping.
*
* @private
*/
private onping(): void {
this.emitReserved("ping")
}
/**
* Called with data.
*
* @private
*/
private ondata(data): void {
this.decoder.add(data)
}
/**
* Called when parser fully decodes a packet.
*
* @private
*/
private ondecoded(packet): void {
this.emitReserved("packet", packet)
}
/**
* Called upon socket error.
*
* @private
*/
private onerror(err): void {
debug("error", err)
this.emitReserved("error", err)
}
/**
* Creates a new socket for the given `nsp`.
*
* @return {Socket}
* @public
*/
public socket(nsp: string, opts?: Partial<SocketOptions>): Socket {
let socket = this.nsps[nsp]
if (!socket) {
socket = new Socket(this, nsp, opts)
this.nsps[nsp] = socket
}
return socket
}
/**
* Called upon a socket close.
*
* @param socket
* @private
*/
_destroy(socket: Socket): void {
const nsps = Object.keys(this.nsps)
for (const nsp of nsps) {
const socket = this.nsps[nsp]
if (socket.active) {
debug("socket %s is still active, skipping close", nsp)
return
}
}
this._close()
}
/**
* Writes a packet.
*
* @param packet
* @private
*/
_packet(packet: Partial<Packet & { query: string; options: any }>): void {
debug("writing packet %j", packet)
const encodedPackets = this.encoder.encode(packet as Packet)
for (let i = 0; i < encodedPackets.length; i++) {
this.engine.write(encodedPackets[i], packet.options)
}
}
/**
* Clean up transport subscriptions and packet buffer.
*
* @private
*/
private cleanup(): void {
debug("cleanup")
this.subs.forEach((subDestroy) => subDestroy())
this.subs.length = 0
this.decoder.destroy()
}
/**
* Close the current socket.
*
* @private
*/
_close(): void {
debug("disconnect")
this.skipReconnect = true
this._reconnecting = false
if ("opening" === this._readyState) {
// `onclose` will not fire because
// an open event never happened
this.cleanup()
}
this.backoff.reset()
this._readyState = "closed"
if (this.engine) this.engine.close()
}
/**
* Alias for close()
*
* @private
*/
private disconnect(): void {
return this._close()
}
/**
* Called upon engine close.
*
* @private
*/
private onclose(reason: string): void {
debug("onclose")
this.cleanup()
this.backoff.reset()
this._readyState = "closed"
this.emitReserved("close", reason)
if (this._reconnection && !this.skipReconnect) {
this.reconnect()
}
}
/**
* Attempt a reconnection.
*
* @private
*/
private reconnect(): this | void {
if (this._reconnecting || this.skipReconnect) return this
const self = this
if (this.backoff.attempts >= this._reconnectionAttempts) {
debug("reconnect failed")
this.backoff.reset()
this.emitReserved("reconnect_failed")
this._reconnecting = false
} else {
const delay = this.backoff.duration()
debug("will wait %dms before reconnect attempt", delay)
this._reconnecting = true
const timer = setTimeout(() => {
if (self.skipReconnect) return
debug("attempting reconnect")
this.emitReserved("reconnect_attempt", self.backoff.attempts)
// check again for the case socket closed in above events
if (self.skipReconnect) return
self.open((err) => {
if (err) {
debug("reconnect attempt error")
self._reconnecting = false
self.reconnect()
this.emitReserved("reconnect_error", err)
} else {
debug("reconnect success")
self.onreconnect()
}
})
}, delay)
if (this.opts.autoUnref) {
timer.unref()
}
this.subs.push(function subDestroy() {
clearTimeout(timer)
})
}
}
/**
* Called upon successful reconnect.
*
* @private
*/
private onreconnect(): void {
const attempt = this.backoff.attempts
this._reconnecting = false
this.backoff.reset()
this.emitReserved("reconnect", attempt)
}
}

View File

@@ -0,0 +1,14 @@
// import type * as Emitter from "component-emitter";
import { EventEmitter } from "events"
import { StrictEventEmitter } from "./typed-events"
export function on(
obj: EventEmitter | StrictEventEmitter<any, any>,
ev: string,
fn: (err?: any) => any
): VoidFunction {
obj.on(ev, fn)
return function subDestroy(): void {
obj.off(ev, fn)
}
}

View File

@@ -0,0 +1,558 @@
import { Packet, PacketType } from "../socket.io-parser"
import { on } from "./on"
import { Manager } from "./manager"
import {
DefaultEventsMap,
EventNames,
EventParams,
EventsMap,
StrictEventEmitter,
} from "./typed-events"
const debug = require("../debug")("socket.io-client")
export interface SocketOptions {
/**
* the authentication payload sent when connecting to the Namespace
*/
auth: { [key: string]: any } | ((cb: (data: object) => void) => void)
}
/**
* Internal events.
* These events can't be emitted by the user.
*/
const RESERVED_EVENTS = Object.freeze({
connect: 1,
connect_error: 1,
disconnect: 1,
disconnecting: 1,
// EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
newListener: 1,
removeListener: 1,
})
interface Flags {
compress?: boolean
volatile?: boolean
}
interface SocketReservedEvents {
connect: () => void
connect_error: (err: Error) => void
disconnect: (reason: Socket.DisconnectReason) => void
}
export class Socket<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<ListenEvents, EmitEvents, SocketReservedEvents> {
public readonly io: Manager<ListenEvents, EmitEvents>
public id: string
public connected: boolean
public disconnected: boolean
public auth: { [key: string]: any } | ((cb: (data: object) => void) => void)
public receiveBuffer: Array<ReadonlyArray<any>> = [];
public sendBuffer: Array<Packet> = [];
private readonly nsp: string
private ids: number = 0;
private acks: object = {};
private flags: Flags = {};
private subs?: Array<VoidFunction>
private _anyListeners: Array<(...args: any[]) => void>
/**
* `Socket` constructor.
*
* @public
*/
constructor(io: Manager, nsp: string, opts?: Partial<SocketOptions>) {
super()
this.io = io
this.nsp = nsp
this.ids = 0
this.acks = {}
this.receiveBuffer = []
this.sendBuffer = []
this.connected = false
this.disconnected = true
this.flags = {}
if (opts && opts.auth) {
this.auth = opts.auth
}
if (this.io._autoConnect) this.open()
}
/**
* Subscribe to open, close and packet events
*
* @private
*/
private subEvents(): void {
if (this.subs) return
const io = this.io
this.subs = [
on(io, "open", this.onopen.bind(this)),
on(io, "packet", this.onpacket.bind(this)),
on(io, "error", this.onerror.bind(this)),
on(io, "close", this.onclose.bind(this)),
]
}
/**
* Whether the Socket will try to reconnect when its Manager connects or reconnects
*/
public get active(): boolean {
return !!this.subs
}
/**
* "Opens" the socket.
*
* @public
*/
public connect(): this {
if (this.connected) return this
this.subEvents()
if (!this.io["_reconnecting"]) this.io.open() // ensure open
if ("open" === this.io._readyState) this.onopen()
return this
}
/**
* Alias for connect()
*/
public open(): this {
return this.connect()
}
/**
* Sends a `message` event.
*
* @return self
* @public
*/
public send(...args: any[]): this {
args.unshift("message")
// @ts-ignore
this.emit.apply(this, args)
return this
}
/**
* Override `emit`.
* If the event is in `events`, it's emitted normally.
*
* @return self
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): this {
if (RESERVED_EVENTS.hasOwnProperty(ev)) {
throw new Error('"' + ev + '" is a reserved event name')
}
args.unshift(ev)
const packet: any = {
type: PacketType.EVENT,
data: args,
}
packet.options = {}
packet.options.compress = this.flags.compress !== false
// event ack callback
if ("function" === typeof args[args.length - 1]) {
debug("emitting packet with ack id %d", this.ids)
this.acks[this.ids] = args.pop()
packet.id = this.ids++
}
const isTransportWritable =
this.io.engine &&
this.io.engine.transport &&
this.io.engine.transport.writable
const discardPacket =
this.flags.volatile && (!isTransportWritable || !this.connected)
if (discardPacket) {
debug("discard packet as the transport is not currently writable")
} else if (this.connected) {
this.packet(packet)
} else {
this.sendBuffer.push(packet)
}
this.flags = {}
return this
}
/**
* Sends a packet.
*
* @param packet
* @private
*/
private packet(packet: Partial<Packet>): void {
packet.nsp = this.nsp
this.io._packet(packet)
}
/**
* Called upon engine `open`.
*
* @private
*/
private onopen(): void {
debug("transport is open - connecting")
if (typeof this.auth == "function") {
this.auth((data) => {
this.packet({ type: PacketType.CONNECT, data })
})
} else {
this.packet({ type: PacketType.CONNECT, data: this.auth })
}
}
/**
* Called upon engine or manager `error`.
*
* @param err
* @private
*/
private onerror(err: Error): void {
if (!this.connected) {
this.emitReserved("connect_error", err)
}
}
/**
* Called upon engine `close`.
*
* @param reason
* @private
*/
private onclose(reason: Socket.DisconnectReason): void {
debug("close (%s)", reason)
this.connected = false
this.disconnected = true
delete this.id
this.emitReserved("disconnect", reason)
}
/**
* Called with socket packet.
*
* @param packet
* @private
*/
private onpacket(packet: Packet): void {
const sameNamespace = packet.nsp === this.nsp
if (!sameNamespace) return
switch (packet.type) {
case PacketType.CONNECT:
if (packet.data && packet.data.sid) {
const id = packet.data.sid
this.onconnect(id)
} else {
this.emitReserved(
"connect_error",
new Error(
"It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"
)
)
}
break
case PacketType.EVENT:
this.onevent(packet)
break
case PacketType.BINARY_EVENT:
this.onevent(packet)
break
case PacketType.ACK:
this.onack(packet)
break
case PacketType.BINARY_ACK:
this.onack(packet)
break
case PacketType.DISCONNECT:
this.ondisconnect()
break
case PacketType.CONNECT_ERROR:
const err = new Error(packet.data.message)
// @ts-ignore
err.data = packet.data.data
this.emitReserved("connect_error", err)
break
}
}
/**
* Called upon a server event.
*
* @param packet
* @private
*/
private onevent(packet: Packet): void {
const args: Array<any> = packet.data || []
debug("emitting event %j", args)
if (null != packet.id) {
debug("attaching ack callback to event")
args.push(this.ack(packet.id))
}
if (this.connected) {
this.emitEvent(args)
} else {
this.receiveBuffer.push(Object.freeze(args))
}
}
private emitEvent(args: ReadonlyArray<any>): void {
if (this._anyListeners && this._anyListeners.length) {
const listeners = this._anyListeners.slice()
for (const listener of listeners) {
// @ts-ignore
listener.apply(this, args)
}
}
// @ts-ignore
super.emit.apply(this, args)
}
/**
* Produces an ack callback to emit with an event.
*
* @private
*/
private ack(id: number): (...args: any[]) => void {
const self = this
let sent = false
return function (...args: any[]) {
// prevent double callbacks
if (sent) return
sent = true
debug("sending ack %j", args)
self.packet({
type: PacketType.ACK,
id: id,
data: args,
})
}
}
/**
* Called upon a server acknowlegement.
*
* @param packet
* @private
*/
private onack(packet: Packet): void {
const ack = this.acks[packet.id]
if ("function" === typeof ack) {
debug("calling ack %s with %j", packet.id, packet.data)
ack.apply(this, packet.data)
delete this.acks[packet.id]
} else {
debug("bad ack %s", packet.id)
}
}
/**
* Called upon server connect.
*
* @private
*/
private onconnect(id: string): void {
debug("socket connected with id %s", id)
this.id = id
this.connected = true
this.disconnected = false
this.emitBuffered()
this.emitReserved("connect")
}
/**
* Emit buffered events (received and emitted).
*
* @private
*/
private emitBuffered(): void {
this.receiveBuffer.forEach((args) => this.emitEvent(args))
this.receiveBuffer = []
this.sendBuffer.forEach((packet) => this.packet(packet))
this.sendBuffer = []
}
/**
* Called upon server disconnect.
*
* @private
*/
private ondisconnect(): void {
debug("server disconnect (%s)", this.nsp)
this.destroy()
this.onclose("io server disconnect")
}
/**
* Called upon forced client/server side disconnections,
* this method ensures the manager stops tracking us and
* that reconnections don't get triggered for this.
*
* @private
*/
private destroy(): void {
if (this.subs) {
// clean subscriptions to avoid reconnections
this.subs.forEach((subDestroy) => subDestroy())
this.subs = undefined
}
this.io["_destroy"](this)
}
/**
* Disconnects the socket manually.
*
* @return self
* @public
*/
public disconnect(): this {
if (this.connected) {
debug("performing disconnect (%s)", this.nsp)
this.packet({ type: PacketType.DISCONNECT })
}
// remove socket from pool
this.destroy()
if (this.connected) {
// fire events
this.onclose("io client disconnect")
}
return this
}
/**
* Alias for disconnect()
*
* @return self
* @public
*/
public close(): this {
return this.disconnect()
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
*/
public compress(compress: boolean): this {
this.flags.compress = compress
return this
}
/**
* Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not
* ready to send messages.
*
* @returns self
* @public
*/
public get volatile(): this {
this.flags.volatile = true
return this
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback.
*
* @param listener
* @public
*/
public onAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || []
this._anyListeners.push(listener)
return this
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
*
* @param listener
* @public
*/
public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || []
this._anyListeners.unshift(listener)
return this
}
/**
* Removes the listener that will be fired when any event is emitted.
*
* @param listener
* @public
*/
public offAny(listener?: (...args: any[]) => void): this {
if (!this._anyListeners) {
return this
}
if (listener) {
const listeners = this._anyListeners
for (let i = 0; i < listeners.length; i++) {
if (listener === listeners[i]) {
listeners.splice(i, 1)
return this
}
}
} else {
this._anyListeners = []
}
return this
}
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*
* @public
*/
public listenersAny() {
return this._anyListeners || []
}
}
export namespace Socket {
export type DisconnectReason =
| "io server disconnect"
| "io client disconnect"
| "ping timeout"
| "transport close"
| "transport error"
}

View File

@@ -0,0 +1,157 @@
import { EventEmitter } from "events"
/**
* An events map is an interface that maps event names to their value, which
* represents the type of the `on` listener.
*/
export interface EventsMap {
[event: string]: any
}
/**
* The default events map, used if no EventsMap is given. Using this EventsMap
* is equivalent to accepting all event names, and any data.
*/
export interface DefaultEventsMap {
[event: string]: (...args: any[]) => void
}
/**
* Returns a union type containing all the keys of an event map.
*/
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol)
/** The tuple type representing the parameters of an event listener */
export type EventParams<
Map extends EventsMap,
Ev extends EventNames<Map>
> = Parameters<Map[Ev]>
/**
* The event names that are either in ReservedEvents or in UserEvents
*/
export type ReservedOrUserEventNames<
ReservedEventsMap extends EventsMap,
UserEvents extends EventsMap
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>
/**
* Type of a listener of a user event or a reserved event. If `Ev` is in
* `ReservedEvents`, the reserved event listener is returned.
*/
export type ReservedOrUserListener<
ReservedEvents extends EventsMap,
UserEvents extends EventsMap,
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
> = FallbackToUntypedListener<
Ev extends EventNames<ReservedEvents>
? ReservedEvents[Ev]
: Ev extends EventNames<UserEvents>
? UserEvents[Ev]
: never
>
/**
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
*
* This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
*/
type FallbackToUntypedListener<T> = [T] extends [never]
? (...args: any[]) => void
: T
/**
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
* parameters for mappings of event names to event data types, and strictly
* types method calls to the `EventEmitter` according to these event maps.
*
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
* listened to with `on` or `once`
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
* emitted with `emit`
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
* emitted by socket.io with `emitReserved`, and can be listened to with
* `listen`.
*/
export abstract class StrictEventEmitter<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ReservedEvents extends EventsMap = {}
> extends EventEmitter {
/**
* Adds the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
super.on(ev as string, listener)
return this
}
/**
* Adds a one-time `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
super.once(ev as string, listener)
return this
}
/**
* Emits an event.
*
* @param ev Name of the event
* @param args Values to send to listeners of this event
*/
// @ts-ignore
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): this {
super.emit(ev as string, ...args)
return this
}
/**
* Emits a reserved event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can emit its own reserved events.
*
* @param ev Reserved event name
* @param args Arguments to emit along with the event
*/
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
ev: Ev,
...args: EventParams<ReservedEvents, Ev>
): this {
super.emit(ev as string, ...args)
return this
}
/**
* Returns the listeners listening to an event.
*
* @param event Event name
* @returns Array of listeners subscribed to `event`
*/
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
event: Ev
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[] {
return super.listeners(event as string) as ReservedOrUserListener<
ReservedEvents,
ListenEvents,
Ev
>[]
}
}

View File

@@ -0,0 +1,97 @@
import * as parseuri from "parseuri"
const debug = require("../debug")("socket.io-client")
type ParsedUrl = {
source: string
protocol: string
authority: string
userInfo: string
user: string
password: string
host: string
port: string
relative: string
path: string
directory: string
file: string
query: string
anchor: string
pathNames: Array<string>
queryKey: { [key: string]: string }
// Custom properties (not native to parseuri):
id: string
href: string
}
/**
* URL parser.
*
* @param uri - url
* @param path - the request path of the connection
* @param loc - An object meant to mimic window.location.
* Defaults to window.location.
* @public
*/
export function url(
uri: string | ParsedUrl,
path: string = "",
loc?: Location
): ParsedUrl {
let obj = uri as ParsedUrl
// default to window.location
loc = loc || (typeof location !== "undefined" && location)
if (null == uri) uri = loc.protocol + "//" + loc.host
// relative path support
if (typeof uri === "string") {
if ("/" === uri.charAt(0)) {
if ("/" === uri.charAt(1)) {
uri = loc.protocol + uri
} else {
uri = loc.host + uri
}
}
if (!/^(https?|wss?):\/\//.test(uri)) {
debug("protocol-less url %s", uri)
if ("undefined" !== typeof loc) {
uri = loc.protocol + "//" + uri
} else {
uri = "https://" + uri
}
}
// parse
debug("parse %s", uri)
obj = parseuri(uri) as ParsedUrl
}
// make sure we treat `localhost:80` and `localhost` equally
if (!obj.port) {
if (/^(http|ws)$/.test(obj.protocol)) {
obj.port = "80"
} else if (/^(http|ws)s$/.test(obj.protocol)) {
obj.port = "443"
}
}
obj.path = obj.path || "/"
const ipv6 = obj.host.indexOf(":") !== -1
const host = ipv6 ? "[" + obj.host + "]" : obj.host
// define unique id
obj.id = obj.protocol + "://" + host + ":" + obj.port + path
// define href
obj.href =
obj.protocol +
"://" +
host +
(loc && loc.port === obj.port ? "" : ":" + obj.port)
return obj
}

View File

@@ -200,7 +200,7 @@ export class Client<
* @private
*/
private close(): void {
console.debug(`client ${this.id} clise - reason: forcing transport close`)
console.debug(`client ${this.id} close - reason: forcing transport close`)
if ("open" === this.conn.readyState) {
console.debug("forcing transport close")
this.conn.close()

View File

@@ -2,6 +2,9 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "src",
"outDir": "dist"
"outDir": "dist",
"lib": [
"dom"
]
}
}
}