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

190 lines
5.2 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)
}
if (stanza.name === 'message') {
this.receiveMessageStanza(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)
}
}
}
protected receiveMessageStanza (stanza: XMPPStanza, fromResource?: string): void {
if (stanza.attrs.type !== 'groupchat') {
return
}
// ignoring messages send by the bot himself
if (stanza.attrs.from === this.userJID?.toString()) {
return
}
// ignoring history messages
if (stanza.getChild('delay')) {
return
}
const body = stanza.getChild('body')
// ignoring message without body (subject, ...)
if (!body) {
return
}
let mentionned: boolean = false // I'm I mentionned?
// TODO: fix this ugly code.
if (this.userJID) {
const references = stanza.getChildren('reference')
for (const reference of references) {
if (reference.attrs.type === 'mention') {
if (reference.attrs.uri === 'xmpp:' + this.userJID.toString()) {
mentionned = true
} else {
const addr = this.component.getAddress()
if (addr) {
if (reference.attrs.uri === 'xmpp:' + addr.toString()) {
mentionned = true
}
}
}
}
}
}
const user = fromResource ? this.roster.get(fromResource) : undefined
this.emit('room_message', body.toString(), user, mentionned)
}
public attachHandler (handler: BotHandler): void {
this.handlers.push(handler)
}
public detachHandlers (): void {
for (const handler of this.handlers) {
handler.stop()
}
}
}