mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-09 16:48:50 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86235314c4 | ||
|
|
b39dac3551 | ||
|
|
929d62bbbb | ||
|
|
b575e47844 | ||
|
|
b076c34a2f | ||
|
|
4bb3eb2d40 | ||
|
|
87f2c74bb3 |
@@ -133,6 +133,14 @@ import WebSocket from 'ws'
|
|||||||
useWebSocketImplementation(WebSocket)
|
useWebSocketImplementation(WebSocket)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can enable regular pings of connected relays with the `enablePing` option. This will set up a heartbeat that closes the websocket if it doesn't receive a response in time. Some platforms don't report websocket disconnections due to network issues, and enabling this can increase reliability.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { SimplePool } from 'nostr-tools/pool'
|
||||||
|
|
||||||
|
const pool = new SimplePool({ enablePing: true })
|
||||||
|
```
|
||||||
|
|
||||||
### Parsing references (mentions) from a content based on NIP-27
|
### Parsing references (mentions) from a content based on NIP-27
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export class AbstractSimplePool {
|
|||||||
public trackRelays: boolean = false
|
public trackRelays: boolean = false
|
||||||
|
|
||||||
public verifyEvent: Nostr['verifyEvent']
|
public verifyEvent: Nostr['verifyEvent']
|
||||||
|
public enablePing: boolean | undefined
|
||||||
public trustedRelayURLs: Set<string> = new Set()
|
public trustedRelayURLs: Set<string> = new Set()
|
||||||
|
|
||||||
private _WebSocket?: typeof WebSocket
|
private _WebSocket?: typeof WebSocket
|
||||||
@@ -39,6 +40,7 @@ export class AbstractSimplePool {
|
|||||||
constructor(opts: AbstractPoolConstructorOptions) {
|
constructor(opts: AbstractPoolConstructorOptions) {
|
||||||
this.verifyEvent = opts.verifyEvent
|
this.verifyEvent = opts.verifyEvent
|
||||||
this._WebSocket = opts.websocketImplementation
|
this._WebSocket = opts.websocketImplementation
|
||||||
|
this.enablePing = opts.enablePing
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise<AbstractRelay> {
|
async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise<AbstractRelay> {
|
||||||
@@ -49,6 +51,7 @@ export class AbstractSimplePool {
|
|||||||
relay = new AbstractRelay(url, {
|
relay = new AbstractRelay(url, {
|
||||||
verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent,
|
verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent,
|
||||||
websocketImplementation: this._WebSocket,
|
websocketImplementation: this._WebSocket,
|
||||||
|
enablePing: this.enablePing,
|
||||||
})
|
})
|
||||||
relay.onclose = () => {
|
relay.onclose = () => {
|
||||||
this.relays.delete(url)
|
this.relays.delete(url)
|
||||||
@@ -71,19 +74,32 @@ export class AbstractSimplePool {
|
|||||||
subscribe(relays: string[], filter: Filter, params: SubscribeManyParams): SubCloser {
|
subscribe(relays: string[], filter: Filter, params: SubscribeManyParams): SubCloser {
|
||||||
params.onauth = params.onauth || params.doauth
|
params.onauth = params.onauth || params.doauth
|
||||||
|
|
||||||
return this.subscribeMap(
|
const request: { url: string; filter: Filter }[] = []
|
||||||
relays.map(url => ({ url, filter })),
|
for (let i = 0; i < relays.length; i++) {
|
||||||
params,
|
const url = normalizeURL(relays[i])
|
||||||
)
|
if (!request.find(r => r.url === url)) {
|
||||||
|
request.push({ url, filter })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.subscribeMap(request, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
|
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
|
||||||
params.onauth = params.onauth || params.doauth
|
params.onauth = params.onauth || params.doauth
|
||||||
|
|
||||||
return this.subscribeMap(
|
const request: { url: string; filter: Filter }[] = []
|
||||||
relays.flatMap(url => filters.map(filter => ({ url, filter }))),
|
const uniqUrls: string[] = []
|
||||||
params,
|
for (let i = 0; i < relays.length; i++) {
|
||||||
)
|
const url = normalizeURL(relays[i])
|
||||||
|
if (uniqUrls.indexOf(url) === -1) {
|
||||||
|
for (let f = 0; f < filters.length; f++) {
|
||||||
|
request.push({ url, filter: filters[f] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.subscribeMap(request, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeMap(requests: { url: string; filter: Filter }[], params: SubscribeManyParams): SubCloser {
|
subscribeMap(requests: { url: string; filter: Filter }[], params: SubscribeManyParams): SubCloser {
|
||||||
@@ -137,8 +153,6 @@ export class AbstractSimplePool {
|
|||||||
// open a subscription in all given relays
|
// open a subscription in all given relays
|
||||||
const allOpened = Promise.all(
|
const allOpened = Promise.all(
|
||||||
requests.map(async ({ url, filter }, i) => {
|
requests.map(async ({ url, filter }, i) => {
|
||||||
url = normalizeURL(url)
|
|
||||||
|
|
||||||
let relay: AbstractRelay
|
let relay: AbstractRelay
|
||||||
try {
|
try {
|
||||||
relay = await this.ensureRelay(url, {
|
relay = await this.ensureRelay(url, {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type RelayWebSocket = WebSocket & {
|
|||||||
export type AbstractRelayConstructorOptions = {
|
export type AbstractRelayConstructorOptions = {
|
||||||
verifyEvent: Nostr['verifyEvent']
|
verifyEvent: Nostr['verifyEvent']
|
||||||
websocketImplementation?: typeof WebSocket
|
websocketImplementation?: typeof WebSocket
|
||||||
|
enablePing?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SendingOnClosedConnection extends Error {
|
export class SendingOnClosedConnection extends Error {
|
||||||
@@ -34,7 +35,10 @@ export class AbstractRelay {
|
|||||||
public baseEoseTimeout: number = 4400
|
public baseEoseTimeout: number = 4400
|
||||||
public connectionTimeout: number = 4400
|
public connectionTimeout: number = 4400
|
||||||
public publishTimeout: number = 4400
|
public publishTimeout: number = 4400
|
||||||
|
public pingFrequency: number = 20000
|
||||||
|
public pingTimeout: number = 20000
|
||||||
public openSubs: Map<string, Subscription> = new Map()
|
public openSubs: Map<string, Subscription> = new Map()
|
||||||
|
public enablePing: boolean | undefined
|
||||||
private connectionTimeoutHandle: ReturnType<typeof setTimeout> | undefined
|
private connectionTimeoutHandle: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
private connectionPromise: Promise<void> | undefined
|
private connectionPromise: Promise<void> | undefined
|
||||||
@@ -54,9 +58,7 @@ export class AbstractRelay {
|
|||||||
this.url = normalizeURL(url)
|
this.url = normalizeURL(url)
|
||||||
this.verifyEvent = opts.verifyEvent
|
this.verifyEvent = opts.verifyEvent
|
||||||
this._WebSocket = opts.websocketImplementation || WebSocket
|
this._WebSocket = opts.websocketImplementation || WebSocket
|
||||||
// this.pingHeartBeat = opts.pingHeartBeat
|
this.enablePing = opts.enablePing
|
||||||
// this.pingFrequency = opts.pingFrequency
|
|
||||||
// this.pingTimeout = opts.pingTimeout
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async connect(url: string, opts: AbstractRelayConstructorOptions): Promise<AbstractRelay> {
|
static async connect(url: string, opts: AbstractRelayConstructorOptions): Promise<AbstractRelay> {
|
||||||
@@ -110,8 +112,7 @@ export class AbstractRelay {
|
|||||||
this.ws.onopen = () => {
|
this.ws.onopen = () => {
|
||||||
clearTimeout(this.connectionTimeoutHandle)
|
clearTimeout(this.connectionTimeoutHandle)
|
||||||
this._connected = true
|
this._connected = true
|
||||||
if (this.ws && this.ws.ping) {
|
if (this.enablePing) {
|
||||||
// && this.pingHeartBeat
|
|
||||||
this.pingpong()
|
this.pingpong()
|
||||||
}
|
}
|
||||||
resolve()
|
resolve()
|
||||||
@@ -145,9 +146,26 @@ export class AbstractRelay {
|
|||||||
return this.connectionPromise
|
return this.connectionPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
private async receivePong() {
|
private async waitForPingPong() {
|
||||||
return new Promise((res, err) => {
|
return new Promise((res, err) => {
|
||||||
|
// listen for pong
|
||||||
;(this.ws && this.ws.on && this.ws.on('pong', () => res(true))) || err("ws can't listen for pong")
|
;(this.ws && this.ws.on && this.ws.on('pong', () => res(true))) || err("ws can't listen for pong")
|
||||||
|
// send a ping
|
||||||
|
this.ws && this.ws.ping && this.ws.ping()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForDummyReq() {
|
||||||
|
return new Promise((resolve, _) => {
|
||||||
|
// make a dummy request with expected empty eose reply
|
||||||
|
// ["REQ", "_", {"ids":["aaaa...aaaa"]}]
|
||||||
|
const sub = this.subscribe([{ ids: ['a'.repeat(64)] }], {
|
||||||
|
oneose: () => {
|
||||||
|
sub.close()
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
eoseTimeout: this.pingTimeout + 1000,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,21 +173,22 @@ export class AbstractRelay {
|
|||||||
// in browsers it's done automatically. see https://github.com/nbd-wtf/nostr-tools/issues/491
|
// in browsers it's done automatically. see https://github.com/nbd-wtf/nostr-tools/issues/491
|
||||||
private async pingpong() {
|
private async pingpong() {
|
||||||
// if the websocket is connected
|
// if the websocket is connected
|
||||||
if (this.ws?.readyState == 1) {
|
if (this.ws?.readyState === 1) {
|
||||||
// send a ping
|
// wait for either a ping-pong reply or a timeout
|
||||||
this.ws && this.ws.ping && this.ws.ping()
|
|
||||||
// wait for either a pong or a timeout
|
|
||||||
const result = await Promise.any([
|
const result = await Promise.any([
|
||||||
this.receivePong(),
|
// browsers don't have ping so use a dummy req
|
||||||
new Promise(res => setTimeout(() => res(false), 10000)), // TODO: opts.pingTimeout
|
this.ws && this.ws.ping && this.ws.on ? this.waitForPingPong() : this.waitForDummyReq(),
|
||||||
|
new Promise(res => setTimeout(() => res(false), this.pingTimeout)),
|
||||||
])
|
])
|
||||||
console.error('pingpong result', result)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
// schedule another pingpong
|
// schedule another pingpong
|
||||||
setTimeout(() => this.pingpong(), 10000) // TODO: opts.pingFrequency
|
setTimeout(() => this.pingpong(), this.pingFrequency)
|
||||||
} else {
|
} else {
|
||||||
// pingpong closing socket
|
// pingpong closing socket
|
||||||
this.ws && this.ws.close()
|
this.closeAllSubscriptions('pingpong timed out')
|
||||||
|
this._connected = false
|
||||||
|
this.ws?.close()
|
||||||
|
this.onclose?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
jsr.json
2
jsr.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nostr/tools",
|
"name": "@nostr/tools",
|
||||||
"version": "2.15.2",
|
"version": "2.16.2",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts",
|
".": "./index.ts",
|
||||||
"./core": "./core.ts",
|
"./core": "./core.ts",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
// prettier-ignore
|
||||||
import {
|
import {
|
||||||
decode,
|
decode,
|
||||||
naddrEncode,
|
naddrEncode,
|
||||||
|
|||||||
109
nip57.test.ts
109
nip57.test.ts
@@ -1,112 +1,7 @@
|
|||||||
import { describe, test, expect, mock } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import { finalizeEvent } from './pure.ts'
|
import { finalizeEvent } from './pure.ts'
|
||||||
import { getPublicKey, generateSecretKey } from './pure.ts'
|
import { getPublicKey, generateSecretKey } from './pure.ts'
|
||||||
import {
|
import { getSatoshisAmountFromBolt11, makeZapReceipt, validateZapRequest } from './nip57.ts'
|
||||||
getSatoshisAmountFromBolt11,
|
|
||||||
getZapEndpoint,
|
|
||||||
makeZapReceipt,
|
|
||||||
makeZapRequest,
|
|
||||||
useFetchImplementation,
|
|
||||||
validateZapRequest,
|
|
||||||
} from './nip57.ts'
|
|
||||||
import { buildEvent } from './test-helpers.ts'
|
|
||||||
|
|
||||||
describe('getZapEndpoint', () => {
|
|
||||||
test('returns null if neither lud06 nor lud16 is present', async () => {
|
|
||||||
const metadata = buildEvent({ kind: 0, content: '{}' })
|
|
||||||
const result = await getZapEndpoint(metadata)
|
|
||||||
|
|
||||||
expect(result).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null if fetch fails', async () => {
|
|
||||||
const fetchImplementation = mock(() => Promise.reject(new Error()))
|
|
||||||
useFetchImplementation(fetchImplementation)
|
|
||||||
|
|
||||||
const metadata = buildEvent({ kind: 0, content: '{"lud16": "name@domain"}' })
|
|
||||||
const result = await getZapEndpoint(metadata)
|
|
||||||
|
|
||||||
expect(result).toBeNull()
|
|
||||||
expect(fetchImplementation).toHaveBeenCalledWith('https://domain/.well-known/lnurlp/name')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null if the response does not allow Nostr payments', async () => {
|
|
||||||
const fetchImplementation = mock(() => Promise.resolve({ json: () => ({ allowsNostr: false }) }))
|
|
||||||
useFetchImplementation(fetchImplementation)
|
|
||||||
|
|
||||||
const metadata = buildEvent({ kind: 0, content: '{"lud16": "name@domain"}' })
|
|
||||||
const result = await getZapEndpoint(metadata)
|
|
||||||
|
|
||||||
expect(result).toBeNull()
|
|
||||||
expect(fetchImplementation).toHaveBeenCalledWith('https://domain/.well-known/lnurlp/name')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns the callback URL if the response allows Nostr payments', async () => {
|
|
||||||
const fetchImplementation = mock(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
json: () => ({
|
|
||||||
allowsNostr: true,
|
|
||||||
nostrPubkey: 'pubkey',
|
|
||||||
callback: 'callback',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
useFetchImplementation(fetchImplementation)
|
|
||||||
|
|
||||||
const metadata = buildEvent({ kind: 0, content: '{"lud16": "name@domain"}' })
|
|
||||||
const result = await getZapEndpoint(metadata)
|
|
||||||
|
|
||||||
expect(result).toBe('callback')
|
|
||||||
expect(fetchImplementation).toHaveBeenCalledWith('https://domain/.well-known/lnurlp/name')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('makeZapRequest', () => {
|
|
||||||
test('throws an error if amount is not given', () => {
|
|
||||||
expect(() =>
|
|
||||||
// @ts-expect-error
|
|
||||||
makeZapRequest({
|
|
||||||
profile: 'profile',
|
|
||||||
event: null,
|
|
||||||
relays: [],
|
|
||||||
comment: '',
|
|
||||||
}),
|
|
||||||
).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('throws an error if profile is not given', () => {
|
|
||||||
expect(() =>
|
|
||||||
// @ts-expect-error
|
|
||||||
makeZapRequest({
|
|
||||||
event: null,
|
|
||||||
amount: 100,
|
|
||||||
relays: [],
|
|
||||||
comment: '',
|
|
||||||
}),
|
|
||||||
).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns a valid Zap request', () => {
|
|
||||||
const result = makeZapRequest({
|
|
||||||
profile: 'profile',
|
|
||||||
event: 'event',
|
|
||||||
amount: 100,
|
|
||||||
relays: ['relay1', 'relay2'],
|
|
||||||
comment: 'comment',
|
|
||||||
})
|
|
||||||
expect(result.kind).toBe(9734)
|
|
||||||
expect(result.created_at).toBeCloseTo(Date.now() / 1000, 0)
|
|
||||||
expect(result.content).toBe('comment')
|
|
||||||
expect(result.tags).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
['p', 'profile'],
|
|
||||||
['amount', '100'],
|
|
||||||
['relays', 'relay1', 'relay2'],
|
|
||||||
['e', 'event'],
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('validateZapRequest', () => {
|
describe('validateZapRequest', () => {
|
||||||
test('returns an error message for invalid JSON', () => {
|
test('returns an error message for invalid JSON', () => {
|
||||||
|
|||||||
54
nip57.ts
54
nip57.ts
@@ -1,6 +1,6 @@
|
|||||||
import { bech32 } from '@scure/base'
|
import { bech32 } from '@scure/base'
|
||||||
|
|
||||||
import { validateEvent, verifyEvent, type Event, type EventTemplate } from './pure.ts'
|
import { NostrEvent, validateEvent, verifyEvent, type Event, type EventTemplate } from './pure.ts'
|
||||||
import { utf8Decoder } from './utils.ts'
|
import { utf8Decoder } from './utils.ts'
|
||||||
import { isReplaceableKind, isAddressableKind } from './kinds.ts'
|
import { isReplaceableKind, isAddressableKind } from './kinds.ts'
|
||||||
|
|
||||||
@@ -42,48 +42,44 @@ export async function getZapEndpoint(metadata: Event): Promise<null | string> {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeZapRequest({
|
type ProfileZap = {
|
||||||
profile,
|
pubkey: string
|
||||||
event,
|
|
||||||
amount,
|
|
||||||
relays,
|
|
||||||
comment = '',
|
|
||||||
}: {
|
|
||||||
profile: string
|
|
||||||
event: string | Event | null
|
|
||||||
amount: number
|
amount: number
|
||||||
comment: string
|
comment?: string
|
||||||
relays: string[]
|
relays: string[]
|
||||||
}): EventTemplate {
|
}
|
||||||
if (!amount) throw new Error('amount not given')
|
|
||||||
if (!profile) throw new Error('profile not given')
|
|
||||||
|
|
||||||
|
type EventZap = {
|
||||||
|
event: NostrEvent
|
||||||
|
amount: number
|
||||||
|
comment?: string
|
||||||
|
relays: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeZapRequest(params: ProfileZap | EventZap): EventTemplate {
|
||||||
let zr: EventTemplate = {
|
let zr: EventTemplate = {
|
||||||
kind: 9734,
|
kind: 9734,
|
||||||
created_at: Math.round(Date.now() / 1000),
|
created_at: Math.round(Date.now() / 1000),
|
||||||
content: comment,
|
content: params.comment || '',
|
||||||
tags: [
|
tags: [
|
||||||
['p', profile],
|
['p', 'pubkey' in params ? params.pubkey : params.event.pubkey],
|
||||||
['amount', amount.toString()],
|
['amount', params.amount.toString()],
|
||||||
['relays', ...relays],
|
['relays', ...params.relays],
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event && typeof event === 'string') {
|
if ('event' in params) {
|
||||||
zr.tags.push(['e', event])
|
zr.tags.push(['e', params.event.id])
|
||||||
}
|
if (isReplaceableKind(params.event.kind)) {
|
||||||
if (event && typeof event === 'object') {
|
const a = ['a', `${params.event.kind}:${params.event.pubkey}:`]
|
||||||
// replacable event
|
|
||||||
if (isReplaceableKind(event.kind)) {
|
|
||||||
const a = ['a', `${event.kind}:${event.pubkey}:`]
|
|
||||||
zr.tags.push(a)
|
zr.tags.push(a)
|
||||||
// addressable event
|
} else if (isAddressableKind(params.event.kind)) {
|
||||||
} else if (isAddressableKind(event.kind)) {
|
let d = params.event.tags.find(([t, v]) => t === 'd' && v)
|
||||||
let d = event.tags.find(([t, v]) => t === 'd' && v)
|
|
||||||
if (!d) throw new Error('d tag not found or is empty')
|
if (!d) throw new Error('d tag not found or is empty')
|
||||||
const a = ['a', `${event.kind}:${event.pubkey}:${d[1]}`]
|
const a = ['a', `${params.event.kind}:${params.event.pubkey}:${d[1]}`]
|
||||||
zr.tags.push(a)
|
zr.tags.push(a)
|
||||||
}
|
}
|
||||||
|
zr.tags.push(['k', params.event.kind.toString()])
|
||||||
}
|
}
|
||||||
|
|
||||||
return zr
|
return zr
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "2.15.2",
|
"version": "2.16.2",
|
||||||
"description": "Tools for making a Nostr client.",
|
"description": "Tools for making a Nostr client.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
4
pool.ts
4
pool.ts
@@ -14,8 +14,8 @@ export function useWebSocketImplementation(websocketImplementation: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SimplePool extends AbstractSimplePool {
|
export class SimplePool extends AbstractSimplePool {
|
||||||
constructor() {
|
constructor(options?: { enablePing?: boolean }) {
|
||||||
super({ verifyEvent, websocketImplementation: _WebSocket })
|
super({ verifyEvent, websocketImplementation: _WebSocket, ...options })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user