more progress on refactor
This commit is contained in:
308
client/src/neko/base.ts
Normal file
308
client/src/neko/base.ts
Normal file
@ -0,0 +1,308 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
|
||||
import { OPCODE } from './data'
|
||||
|
||||
import { EVENT, WebSocketEvents } from './events'
|
||||
|
||||
import {
|
||||
WebSocketMessages,
|
||||
WebSocketPayloads,
|
||||
IdentityPayload,
|
||||
SignalPayload,
|
||||
MemberListPayload,
|
||||
MemberPayload,
|
||||
ControlPayload,
|
||||
} from './messages'
|
||||
|
||||
export interface BaseEvents {
|
||||
info: (...message: any[]) => void
|
||||
warn: (...message: any[]) => void
|
||||
debug: (...message: any[]) => void
|
||||
error: (error: Error) => void
|
||||
}
|
||||
|
||||
export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
||||
protected _ws?: WebSocket
|
||||
protected _peer?: RTCPeerConnection
|
||||
protected _channel?: RTCDataChannel
|
||||
protected _timeout?: NodeJS.Timeout
|
||||
protected _username?: string
|
||||
protected _state: RTCIceConnectionState = 'disconnected'
|
||||
|
||||
get socketOpen() {
|
||||
return typeof this._ws !== 'undefined' && this._ws.readyState === WebSocket.OPEN
|
||||
}
|
||||
|
||||
get peerConnected() {
|
||||
return typeof this._peer !== 'undefined' && this._state === 'connected'
|
||||
}
|
||||
|
||||
get connected() {
|
||||
return this.peerConnected && this.socketOpen
|
||||
}
|
||||
|
||||
public connect(url: string, password: string, username: string) {
|
||||
if (this.socketOpen) {
|
||||
this.emit('warn', `attempting to create websocket while connection open`)
|
||||
return
|
||||
}
|
||||
|
||||
if (username === '') {
|
||||
throw new Error('Must add a username') // TODO: Better handleing
|
||||
}
|
||||
|
||||
this._username = username
|
||||
this[EVENT.CONNECTING]()
|
||||
|
||||
try {
|
||||
this._ws = new WebSocket(`${url}ws?password=${password}`)
|
||||
this.emit('debug', `connecting to ${this._ws.url}`)
|
||||
this._ws.onmessage = this.onMessage.bind(this)
|
||||
this._ws.onerror = event => this.onError.bind(this)
|
||||
this._ws.onclose = event => this.onDisconnected.bind(this, new Error('websocket closed'))
|
||||
this._timeout = setTimeout(this.onTimeout.bind(this), 5000)
|
||||
} catch (err) {
|
||||
this.onDisconnected(err)
|
||||
}
|
||||
}
|
||||
|
||||
public sendData(event: 'wheel' | 'mousemove', data: { x: number; y: number }): void
|
||||
public sendData(event: 'mousedown' | 'mouseup' | 'keydown' | 'keyup', data: { key: number }): void
|
||||
public sendData(event: string, data: any) {
|
||||
if (!this.connected) {
|
||||
this.emit('warn', `attemping to send data while dissconneted`)
|
||||
return
|
||||
}
|
||||
|
||||
let buffer: ArrayBuffer
|
||||
let payload: DataView
|
||||
switch (event) {
|
||||
case 'mousemove':
|
||||
buffer = new ArrayBuffer(7)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.MOVE)
|
||||
payload.setUint16(1, 4, true)
|
||||
payload.setUint16(3, data.x, true)
|
||||
payload.setUint16(5, data.y, true)
|
||||
break
|
||||
case 'wheel':
|
||||
buffer = new ArrayBuffer(7)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.SCROLL)
|
||||
payload.setUint16(1, 4, true)
|
||||
payload.setInt16(3, data.x, true)
|
||||
payload.setInt16(5, data.y, true)
|
||||
break
|
||||
case 'keydown':
|
||||
case 'mousedown':
|
||||
buffer = new ArrayBuffer(5)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.KEY_DOWN)
|
||||
payload.setUint16(1, 1, true)
|
||||
payload.setUint16(3, data.key, true)
|
||||
break
|
||||
case 'keyup':
|
||||
case 'mouseup':
|
||||
buffer = new ArrayBuffer(5)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.KEY_UP)
|
||||
payload.setUint16(1, 1, true)
|
||||
payload.setUint16(3, data.key, true)
|
||||
break
|
||||
default:
|
||||
this.emit('warn', `unknown data event: ${event}`)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (typeof buffer !== 'undefined') {
|
||||
this._channel!.send(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
public sendMessage(event: WebSocketEvents, payload?: WebSocketPayloads) {
|
||||
if (!this.connected) {
|
||||
this.emit('warn', `attemping to send message while dissconneted`)
|
||||
return
|
||||
}
|
||||
this.emit('debug', `sending event '${event}' ${payload ? `with payload: ` : ''}`, payload)
|
||||
this._ws!.send(JSON.stringify({ event, ...payload }))
|
||||
}
|
||||
|
||||
public createPeer() {
|
||||
this.emit('debug', `creating peer`)
|
||||
if (!this.socketOpen) {
|
||||
this.emit(
|
||||
'warn',
|
||||
`attempting to create peer with no websocket: `,
|
||||
this._ws ? `state: ${this._ws.readyState}` : 'no socket',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.peerConnected) {
|
||||
this.emit('warn', `attempting to create peer while connected`)
|
||||
return
|
||||
}
|
||||
|
||||
this._peer = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
||||
})
|
||||
|
||||
this._peer.onicecandidate = event => {
|
||||
if (event.candidate === null && this._peer!.localDescription) {
|
||||
this.emit('debug', `sending event '${EVENT.SIGNAL.PROVIDE}' with payload`, this._peer!.localDescription.sdp)
|
||||
this._ws!.send(
|
||||
JSON.stringify({
|
||||
event: EVENT.SIGNAL.PROVIDE,
|
||||
sdp: this._peer!.localDescription.sdp,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this._peer.oniceconnectionstatechange = event => {
|
||||
this._state = this._peer!.iceConnectionState
|
||||
this.emit('debug', `peer connection state chagned: ${this._state}`)
|
||||
|
||||
switch (this._state) {
|
||||
case 'connected':
|
||||
this.onConnected()
|
||||
break
|
||||
case 'failed':
|
||||
this.onDisconnected(new Error('peer failed'))
|
||||
break
|
||||
case 'disconnected':
|
||||
this.onDisconnected(new Error('peer disconnected'))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this._peer.ontrack = this.onTrack.bind(this)
|
||||
this._peer.addTransceiver('audio', { direction: 'recvonly' })
|
||||
this._peer.addTransceiver('video', { direction: 'recvonly' })
|
||||
|
||||
this._channel = this._peer.createDataChannel('data')
|
||||
this._channel.onerror = this.onError.bind(this)
|
||||
this._channel.onmessage = this.onData.bind(this)
|
||||
this._channel.onclose = this.onDisconnected.bind(this, new Error('peer data channel closed'))
|
||||
|
||||
this._peer
|
||||
.createOffer()
|
||||
.then(d => this._peer!.setLocalDescription(d))
|
||||
.catch(err => this.emit('error', err))
|
||||
}
|
||||
|
||||
private setRemoteDescription(payload: SignalPayload) {
|
||||
if (this.peerConnected) {
|
||||
this.emit('warn', `received ${event} with no peer!`)
|
||||
return
|
||||
}
|
||||
this._peer!.setRemoteDescription({ type: 'answer', sdp: payload.sdp })
|
||||
}
|
||||
|
||||
private onMessage(e: MessageEvent) {
|
||||
const { event, ...payload } = JSON.parse(e.data) as WebSocketMessages
|
||||
|
||||
this.emit('debug', `received websocket event ${event} ${payload ? `with payload: ` : ''}`, payload)
|
||||
|
||||
switch (event) {
|
||||
case EVENT.IDENTITY.PROVIDE:
|
||||
this[EVENT.IDENTITY.PROVIDE](payload as IdentityPayload)
|
||||
this.createPeer()
|
||||
break
|
||||
case EVENT.SIGNAL.ANSWER:
|
||||
this.setRemoteDescription(payload as SignalPayload)
|
||||
break
|
||||
default:
|
||||
// @ts-ignore
|
||||
if (typeof this[event] === 'function') {
|
||||
// @ts-ignore
|
||||
this[event](payload)
|
||||
} else {
|
||||
this[EVENT.MESSAGE](event, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onData(e: MessageEvent) {
|
||||
this[EVENT.DATA](e.data)
|
||||
}
|
||||
|
||||
private onTrack(event: RTCTrackEvent) {
|
||||
this.emit('debug', `received ${event.track.kind} track from peer: ${event.track.id}`, event)
|
||||
const stream = event.streams[0]
|
||||
if (!stream) {
|
||||
this.emit('warn', `no stream provided for track ${event.track.id}(${event.track.label})`)
|
||||
return
|
||||
}
|
||||
this[EVENT.TRACK](event)
|
||||
}
|
||||
|
||||
private onError(event: Event) {
|
||||
this.emit('error', (event as ErrorEvent).error)
|
||||
}
|
||||
|
||||
private onConnected() {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout)
|
||||
}
|
||||
|
||||
if (!this.connected) {
|
||||
this.emit('warn', `onConnected called while being disconnected`)
|
||||
return
|
||||
}
|
||||
|
||||
this.emit('debug', `sending event '${EVENT.IDENTITY.DETAILS}' with payload`, { username: this._username })
|
||||
this._ws!.send(
|
||||
JSON.stringify({
|
||||
event: EVENT.IDENTITY.DETAILS,
|
||||
username: this._username,
|
||||
}),
|
||||
)
|
||||
|
||||
this.emit('debug', `connected`)
|
||||
this[EVENT.CONNECTED]()
|
||||
}
|
||||
|
||||
private onTimeout() {
|
||||
this.emit('debug', `connection timedout`)
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout)
|
||||
}
|
||||
this.onDisconnected(new Error('connection timeout'))
|
||||
}
|
||||
|
||||
protected onDisconnected(reason?: Error) {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout)
|
||||
}
|
||||
|
||||
if (this.socketOpen) {
|
||||
try {
|
||||
this._ws!.close()
|
||||
} catch (err) {}
|
||||
this._ws = undefined
|
||||
}
|
||||
|
||||
if (this.peerConnected) {
|
||||
try {
|
||||
this._peer!.close()
|
||||
} catch (err) {}
|
||||
this._peer = undefined
|
||||
}
|
||||
|
||||
this.emit('debug', `disconnected:`, reason)
|
||||
this[EVENT.DISCONNECTED](reason)
|
||||
}
|
||||
|
||||
protected [EVENT.MESSAGE](event: string, payload: any) {
|
||||
this.emit('warn', `unhandled websocket event '${event}':`, payload)
|
||||
}
|
||||
|
||||
protected abstract [EVENT.CONNECTING](): void
|
||||
protected abstract [EVENT.CONNECTED](): void
|
||||
protected abstract [EVENT.DISCONNECTED](reason?: Error): void
|
||||
protected abstract [EVENT.TRACK](event: RTCTrackEvent): void
|
||||
protected abstract [EVENT.DATA](data: any): void
|
||||
protected abstract [EVENT.IDENTITY.PROVIDE](payload: IdentityPayload): void
|
||||
}
|
6
client/src/neko/data.ts
Normal file
6
client/src/neko/data.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const OPCODE = {
|
||||
MOVE: 0x01,
|
||||
SCROLL: 0x02,
|
||||
KEY_DOWN: 0x03,
|
||||
KEY_UP: 0x04,
|
||||
} as const
|
82
client/src/neko/events.ts
Normal file
82
client/src/neko/events.ts
Normal file
@ -0,0 +1,82 @@
|
||||
export const EVENT = {
|
||||
// Internal Events
|
||||
CONNECTING: 'CONNECTING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
TRACK: 'TRACK',
|
||||
MESSAGE: 'MESSAGE',
|
||||
DATA: 'DATA',
|
||||
|
||||
// Websocket Events
|
||||
SYSTEM: {
|
||||
DISCONNECT: 'system/disconnect',
|
||||
},
|
||||
SIGNAL: {
|
||||
ANSWER: 'signal/answer',
|
||||
PROVIDE: 'signal/provide',
|
||||
},
|
||||
IDENTITY: {
|
||||
PROVIDE: 'identity/provide',
|
||||
DETAILS: 'identity/details',
|
||||
},
|
||||
MEMBER: {
|
||||
LIST: 'member/list',
|
||||
CONNECTED: 'member/connected',
|
||||
DISCONNECTED: 'member/disconnected',
|
||||
},
|
||||
CONTROL: {
|
||||
LOCKED: 'control/locked',
|
||||
RELEASE: 'control/release',
|
||||
REQUEST: 'control/request',
|
||||
REQUESTING: 'control/requesting',
|
||||
GIVE: 'control/give',
|
||||
},
|
||||
CHAT: {
|
||||
MESSAGE: 'chat/message',
|
||||
EMOTE: 'chat/emote',
|
||||
},
|
||||
ADMIN: {
|
||||
BAN: 'admin/ban',
|
||||
KICK: 'admin/kick',
|
||||
LOCK: 'admin/lock',
|
||||
UNLOCK: 'admin/unlock',
|
||||
MUTE: 'admin/mute',
|
||||
UNMUTE: 'admin/unmute',
|
||||
CONTROL: 'admin/control',
|
||||
RELEASE: 'admin/release',
|
||||
GIVE: 'admin/give',
|
||||
},
|
||||
} as const
|
||||
|
||||
export type Events = typeof EVENT
|
||||
|
||||
export type WebSocketEvents =
|
||||
| SystemEvents
|
||||
| ControlEvents
|
||||
| IdentityEvents
|
||||
| MemberEvents
|
||||
| SignalEvents
|
||||
| ChatEvents
|
||||
| AdminEvents
|
||||
|
||||
export type ControlEvents =
|
||||
| typeof EVENT.CONTROL.LOCKED
|
||||
| typeof EVENT.CONTROL.RELEASE
|
||||
| typeof EVENT.CONTROL.REQUEST
|
||||
| typeof EVENT.CONTROL.GIVE
|
||||
|
||||
export type SystemEvents = typeof EVENT.SYSTEM.DISCONNECT
|
||||
export type IdentityEvents = typeof EVENT.IDENTITY.PROVIDE | typeof EVENT.IDENTITY.DETAILS
|
||||
export type MemberEvents = typeof EVENT.MEMBER.LIST | typeof EVENT.MEMBER.CONNECTED | typeof EVENT.MEMBER.DISCONNECTED
|
||||
export type SignalEvents = typeof EVENT.SIGNAL.ANSWER | typeof EVENT.SIGNAL.PROVIDE
|
||||
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
|
||||
export type AdminEvents =
|
||||
| typeof EVENT.ADMIN.BAN
|
||||
| typeof EVENT.ADMIN.KICK
|
||||
| typeof EVENT.ADMIN.LOCK
|
||||
| typeof EVENT.ADMIN.UNLOCK
|
||||
| typeof EVENT.ADMIN.MUTE
|
||||
| typeof EVENT.ADMIN.UNMUTE
|
||||
| typeof EVENT.ADMIN.CONTROL
|
||||
| typeof EVENT.ADMIN.RELEASE
|
||||
| typeof EVENT.ADMIN.GIVE
|
458
client/src/neko/index.ts
Normal file
458
client/src/neko/index.ts
Normal file
@ -0,0 +1,458 @@
|
||||
import Vue from 'vue'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import { BaseClient, BaseEvents } from './base'
|
||||
import { Member } from './types'
|
||||
import { EVENT } from './events'
|
||||
import { accessor } from '~/store'
|
||||
|
||||
import {
|
||||
DisconnectPayload,
|
||||
IdentityPayload,
|
||||
MemberListPayload,
|
||||
MemberDisconnectPayload,
|
||||
MemberPayload,
|
||||
ControlPayload,
|
||||
ControlTargetPayload,
|
||||
ChatPayload,
|
||||
EmotePayload,
|
||||
AdminPayload,
|
||||
AdminTargetPayload,
|
||||
} from './messages'
|
||||
|
||||
interface NekoEvents extends BaseEvents {}
|
||||
|
||||
export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
||||
private $vue!: Vue
|
||||
private $accessor!: typeof accessor
|
||||
|
||||
init(vue: Vue) {
|
||||
this.$vue = vue
|
||||
this.$accessor = vue.$accessor
|
||||
}
|
||||
|
||||
connect(password: string, username: string) {
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? `ws://${process.env.VUE_APP_SERVER}/`
|
||||
: `${/https/gi.test(location.protocol) ? 'wss' : 'ws'}://${location.host}/`
|
||||
|
||||
super.connect(url, password, username)
|
||||
}
|
||||
|
||||
private get id() {
|
||||
return this.$accessor.user.id
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Internal Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.CONNECTING]() {
|
||||
this.$accessor.setConnnecting()
|
||||
}
|
||||
|
||||
protected [EVENT.CONNECTED]() {
|
||||
this.$accessor.setConnected(true)
|
||||
this.$accessor.setConnected(true)
|
||||
|
||||
this.$vue.$notify({
|
||||
group: 'neko',
|
||||
type: 'success',
|
||||
title: 'Successfully connected',
|
||||
duration: 5000,
|
||||
speed: 1000,
|
||||
})
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id: this.id,
|
||||
content: 'connected',
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.DISCONNECTED](reason?: Error) {
|
||||
this.$accessor.setConnected(false)
|
||||
|
||||
this.$accessor.remote.clear()
|
||||
this.$accessor.user.clear()
|
||||
this.$accessor.video.clear()
|
||||
this.$accessor.chat.clear()
|
||||
|
||||
this.$vue.$notify({
|
||||
group: 'neko',
|
||||
type: 'error',
|
||||
title: `Disconnected:`,
|
||||
text: reason ? reason.message : undefined,
|
||||
duration: 5000,
|
||||
speed: 1000,
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.TRACK](event: RTCTrackEvent) {
|
||||
const { track, streams } = event
|
||||
if (track.kind === 'audio') {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.video.addTrack([track, streams[0]])
|
||||
this.$accessor.video.setStream(0)
|
||||
}
|
||||
|
||||
protected [EVENT.DATA](data: any) {}
|
||||
|
||||
/////////////////////////////
|
||||
// System Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.SYSTEM.DISCONNECT]({ message }: DisconnectPayload) {
|
||||
this.onDisconnected(new Error(message))
|
||||
this.$vue.$swal({
|
||||
title: 'Disconnected!',
|
||||
text: message,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'ok',
|
||||
})
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Identity Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.IDENTITY.PROVIDE]({ id }: IdentityPayload) {
|
||||
this.$accessor.user.setMember(id)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Member Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.MEMBER.LIST]({ members }: MemberListPayload) {
|
||||
this.$accessor.user.setMembers(members)
|
||||
}
|
||||
|
||||
protected [EVENT.MEMBER.CONNECTED](member: MemberPayload) {
|
||||
this.$accessor.user.addMember(member)
|
||||
|
||||
if (member.id !== this.id) {
|
||||
this.$accessor.chat.newMessage({
|
||||
id: member.id,
|
||||
content: 'connected',
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
protected [EVENT.MEMBER.DISCONNECTED]({ id }: MemberDisconnectPayload) {
|
||||
const member = this.member(id)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id: member.id,
|
||||
content: 'disconnected',
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
|
||||
this.$accessor.user.delMember(id)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Control Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.CONTROL.LOCKED]({ id }: ControlPayload) {
|
||||
this.$accessor.remote.setHost(id)
|
||||
const member = this.member(id)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.id === id) {
|
||||
this.$vue.$notify({
|
||||
group: 'neko',
|
||||
type: 'info',
|
||||
title: `You have the controls`,
|
||||
duration: 5000,
|
||||
speed: 1000,
|
||||
})
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id: member.id,
|
||||
content: 'took the controls',
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.CONTROL.RELEASE]({ id }: ControlPayload) {
|
||||
this.$accessor.remote.clear()
|
||||
const member = this.member(id)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.id === id) {
|
||||
this.$vue.$notify({
|
||||
group: 'neko',
|
||||
type: 'info',
|
||||
title: `You released the controls`,
|
||||
duration: 5000,
|
||||
speed: 1000,
|
||||
})
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id: member.id,
|
||||
content: 'released the controls',
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.CONTROL.REQUEST]({ id }: ControlPayload) {
|
||||
const member = this.member(id)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$vue.$notify({
|
||||
group: 'neko',
|
||||
type: 'info',
|
||||
title: `${member.username} has the controls`,
|
||||
text: 'But I let them know you wanted it',
|
||||
duration: 5000,
|
||||
speed: 1000,
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.CONTROL.REQUESTING]({ id }: ControlPayload) {
|
||||
const member = this.member(id)
|
||||
if (!member || member.ignored) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$vue.$notify({
|
||||
group: 'neko',
|
||||
type: 'info',
|
||||
title: `${member.username} is requesting the controls`,
|
||||
duration: 5000,
|
||||
speed: 1000,
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.CONTROL.GIVE]({ id, target }: ControlTargetPayload) {
|
||||
const member = this.member(target)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.remote.setHost(member)
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `gave the controls to ${member.id == this.id ? 'you' : member.username}`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Chat Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.CHAT.MESSAGE]({ id, content }: ChatPayload) {
|
||||
const member = this.member(id)
|
||||
if (!member || member.ignored) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content,
|
||||
type: 'text',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.CHAT.EMOTE]({ id, emote }: EmotePayload) {
|
||||
const member = this.member(id)
|
||||
if (!member || member.ignored) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newEmote({ type: emote })
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Admin Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.ADMIN.BAN]({ id, target }: AdminTargetPayload) {
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
const member = this.member(target)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `banned ${member.id == this.id ? 'you' : member.username}`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.ADMIN.KICK]({ id, target }: AdminTargetPayload) {
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
const member = this.member(target)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `kicked ${member.id == this.id ? 'you' : member.username}`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.ADMIN.MUTE]({ id, target }: AdminTargetPayload) {
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.user.setMuted({ id: target, muted: true })
|
||||
|
||||
const member = this.member(target)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `muted ${member.id == this.id ? 'you' : member.username}`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.ADMIN.UNMUTE]({ id, target }: AdminTargetPayload) {
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.user.setMuted({ id: target, muted: false })
|
||||
|
||||
const member = this.member(target)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `unmuted ${member.username}`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.ADMIN.LOCK]({ id }: AdminPayload) {
|
||||
this.$accessor.setLocked(true)
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `locked the room`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.ADMIN.UNLOCK]({ id }: AdminPayload) {
|
||||
this.$accessor.setLocked(false)
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `unlocked the room`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.ADMIN.CONTROL]({ id, target }: AdminTargetPayload) {
|
||||
this.$accessor.remote.setHost(id)
|
||||
|
||||
if (!target) {
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `force took the controls`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const member = this.member(target)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `took the controls from ${member.id == this.id ? 'you' : member.username}`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.ADMIN.RELEASE]({ id, target }: AdminTargetPayload) {
|
||||
this.$accessor.remote.clear()
|
||||
if (!target) {
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `force released the controls`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const member = this.member(target)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `released the controls from ${member.id == this.id ? 'you' : member.username}`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
protected [EVENT.ADMIN.GIVE]({ id, target }: AdminTargetPayload) {
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
const member = this.member(target)
|
||||
if (!member) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$accessor.remote.setHost(member)
|
||||
|
||||
this.$accessor.chat.newMessage({
|
||||
id,
|
||||
content: `gave the controls to ${member.id == this.id ? 'you' : member.username}`,
|
||||
type: 'event',
|
||||
created: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
// Utilities
|
||||
protected member(id: string): Member | undefined {
|
||||
return this.$accessor.user.members[id]
|
||||
}
|
||||
}
|
161
client/src/neko/messages.ts
Normal file
161
client/src/neko/messages.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import {
|
||||
EVENT,
|
||||
WebSocketEvents,
|
||||
SystemEvents,
|
||||
ControlEvents,
|
||||
IdentityEvents,
|
||||
MemberEvents,
|
||||
SignalEvents,
|
||||
ChatEvents,
|
||||
AdminEvents,
|
||||
} from './events'
|
||||
import { Member } from './types'
|
||||
|
||||
export type WebSocketMessages =
|
||||
| WebSocketMessage
|
||||
| IdentityMessage
|
||||
| SignalMessage
|
||||
| MemberListMessage
|
||||
| MembeConnectMessage
|
||||
| MembeDisconnectMessage
|
||||
| ControlMessage
|
||||
| ChatMessage
|
||||
|
||||
export type WebSocketPayloads =
|
||||
| IdentityPayload
|
||||
| SignalPayload
|
||||
| MemberListPayload
|
||||
| Member
|
||||
| ControlPayload
|
||||
| ChatPayload
|
||||
| ChatSendPayload
|
||||
| EmojiSendPayload
|
||||
| AdminPayload
|
||||
|
||||
export interface WebSocketMessage {
|
||||
event: WebSocketEvents | string
|
||||
}
|
||||
|
||||
/*
|
||||
SYSTEM MESSAGES/PAYLOADS
|
||||
*/
|
||||
// system/disconnect
|
||||
export interface DisconnectMessage extends WebSocketMessage, DisconnectPayload {
|
||||
event: typeof EVENT.SYSTEM.DISCONNECT
|
||||
}
|
||||
export interface DisconnectPayload {
|
||||
message: string
|
||||
}
|
||||
|
||||
/*
|
||||
IDENTITY MESSAGES/PAYLOADS
|
||||
*/
|
||||
// identity/provide
|
||||
export interface IdentityMessage extends WebSocketMessage, IdentityPayload {
|
||||
event: typeof EVENT.IDENTITY.PROVIDE
|
||||
}
|
||||
export interface IdentityPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
/*
|
||||
SIGNAL MESSAGES/PAYLOADS
|
||||
*/
|
||||
// signal/answer
|
||||
export interface SignalMessage extends WebSocketMessage, SignalPayload {
|
||||
event: typeof EVENT.SIGNAL.ANSWER
|
||||
}
|
||||
export interface SignalPayload {
|
||||
sdp: string
|
||||
}
|
||||
|
||||
/*
|
||||
MEMBER MESSAGES/PAYLOADS
|
||||
*/
|
||||
// member/list
|
||||
export interface MemberListMessage extends WebSocketMessage, MemberListPayload {
|
||||
event: typeof EVENT.MEMBER.LIST
|
||||
}
|
||||
export interface MemberListPayload {
|
||||
members: Member[]
|
||||
}
|
||||
|
||||
// member/connected
|
||||
export interface MembeConnectMessage extends WebSocketMessage, MemberPayload {
|
||||
event: typeof EVENT.MEMBER.CONNECTED
|
||||
}
|
||||
export type MemberPayload = Member
|
||||
|
||||
// member/disconnected
|
||||
export interface MembeDisconnectMessage extends WebSocketMessage, MemberPayload {
|
||||
event: typeof EVENT.MEMBER.DISCONNECTED
|
||||
}
|
||||
export interface MemberDisconnectPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
/*
|
||||
CONTROL MESSAGES/PAYLOADS
|
||||
*/
|
||||
// control/locked & control/release & control/request
|
||||
export interface ControlMessage extends WebSocketMessage, ControlPayload {
|
||||
event: ControlEvents
|
||||
}
|
||||
export interface ControlPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface ControlTargetPayload {
|
||||
id: string
|
||||
target: string
|
||||
}
|
||||
|
||||
/*
|
||||
CHAT PAYLOADS
|
||||
*/
|
||||
// chat/message
|
||||
export interface ChatMessage extends WebSocketMessage, ChatPayload {
|
||||
event: typeof EVENT.CHAT.MESSAGE
|
||||
}
|
||||
|
||||
export interface ChatSendPayload {
|
||||
content: string
|
||||
}
|
||||
export interface ChatPayload {
|
||||
id: string
|
||||
content: string
|
||||
}
|
||||
|
||||
// chat/emoji
|
||||
export interface ChatEmoteMessage extends WebSocketMessage, EmotePayload {
|
||||
event: typeof EVENT.CHAT.EMOTE
|
||||
}
|
||||
|
||||
export interface EmotePayload {
|
||||
id: string
|
||||
emote: string
|
||||
}
|
||||
|
||||
export interface EmojiSendPayload {
|
||||
emote: string
|
||||
}
|
||||
|
||||
/*
|
||||
ADMIN PAYLOADS
|
||||
*/
|
||||
export interface AdminMessage extends WebSocketMessage, AdminPayload {
|
||||
event: AdminEvents
|
||||
}
|
||||
|
||||
export interface AdminPayload {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface AdminTargetMessage extends WebSocketMessage, AdminTargetPayload {
|
||||
event: AdminEvents
|
||||
}
|
||||
|
||||
export interface AdminTargetPayload {
|
||||
id: string
|
||||
target?: string
|
||||
}
|
8
client/src/neko/types.ts
Normal file
8
client/src/neko/types.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface Member {
|
||||
id: string
|
||||
username: string
|
||||
admin: boolean
|
||||
muted: boolean
|
||||
connected?: boolean
|
||||
ignored?: boolean
|
||||
}
|
Reference in New Issue
Block a user