peertube-plugin-livechat/bots/lib/bot/room.ts
2021-12-08 16:12:47 +01:00

145 lines
3.8 KiB
TypeScript

import type { BotComponent } from './component'
import type { BotHandler } from './handlers/base'
import type { XMPPStanza, XMPPUser } from './types'
import EventEmitter from 'events'
import { JID } from '@xmpp/jid'
import { logger } from '../logger'
export class BotRoom extends EventEmitter {
protected state: 'offline' | 'online' = 'offline'
protected userJID: JID | undefined
protected readonly roster: Map<string, XMPPUser> = new Map()
protected readonly handlers: BotHandler[] = []
constructor (
protected readonly component: BotComponent,
protected readonly roomJID: JID
) {
super()
this.on('reset', () => {
this.state = 'offline'
this.roster.clear()
})
this.on('stanza', (stanza: XMPPStanza, resource?: string) => {
this.receiveStanza(stanza, resource)
})
}
public isOnline (): boolean {
return this.state === 'online'
}
public onlineUserCount (): number {
let count = 0
this.roster.forEach(user => {
if (user.state === 'online') { count++ }
})
return count
}
public async join (nick: string): Promise<void> {
this.userJID = new JID(this.roomJID.getLocal(), this.roomJID.getDomain(), nick)
logger.debug(`Emitting a presence for room ${this.roomJID.toString()}...`)
await this.component.sendStanza('presence',
{
to: this.userJID.toString()
},
this.component.xml('x', {
xmlns: 'http://jabber.org/protocol/muc'
})
)
// FIXME: should wait for a presence stanza from the server.
// FIXME: should handle used nick errors.
}
public async part (): Promise<void> {
if (!this.userJID) { return }
logger.debug(`Emitting a presence=unavailable for room ${this.roomJID.toString()}...`)
await this.component.sendStanza('presence', {
to: this.userJID.toString(),
type: 'unavailable'
})
// FIXME: should wait for a presence stanza from the server.
}
public async sendGroupchat (msg: string): Promise<void> {
if (!this.userJID) { return }
logger.debug(`Emitting a groupchat message for room ${this.roomJID.toString()}...`)
await this.component.sendStanza(
'message',
{
type: 'groupchat',
to: this.roomJID.toString()
},
this.component.xml('body', {}, msg)
)
}
public receiveStanza (stanza: XMPPStanza, fromResource?: string): void {
if (stanza.name === 'presence') {
this.receivePresenceStanza(stanza, fromResource)
}
}
public receivePresenceStanza (stanza: XMPPStanza, fromResource?: string): void {
if (!fromResource) {
return
}
const isPresent = stanza.attrs.type !== 'unavailable'
const statusElems = stanza.getChild('x')?.getChildren('status')
const statusCodes = []
if (statusElems) {
for (const s of statusElems) {
statusCodes.push(parseInt(s.attrs.code))
}
}
const isMe = statusCodes.includes(110) // status 110 means that is concern the current user.
let user = this.roster.get(fromResource)
const previousState = user?.state
if (!isPresent) {
if (!user) {
return
}
user.state = 'offline'
if (isMe) {
this.state = 'offline'
}
if (previousState === 'online') {
this.emit('room_part', user)
}
} else {
if (!user) {
user = {
state: 'online',
nick: fromResource,
isMe: isMe
}
this.roster.set(fromResource, user)
} else {
user.state = 'online'
}
if (isMe) {
this.state = 'online'
}
if (previousState !== 'online') {
this.emit('room_join', user)
}
}
}
public attachHandler (handler: BotHandler): void {
this.handlers.push(handler)
}
public detachHandlers (): void {
for (const handler of this.handlers) {
handler.stop()
}
}
}