Demo Bot: Complete code refactoring. WIP.
This commit is contained in:
parent
2c72f3bf2f
commit
42988a5d04
21
bots/bots.ts
21
bots/bots.ts
@ -1,14 +1,14 @@
|
||||
import { BotsConfig } from './lib/config'
|
||||
import { logger } from './lib/logger'
|
||||
import { ComponentBot } from './lib/bot/component'
|
||||
import { DemoBot } from './lib/bot/demobot'
|
||||
import { BotComponent } from './lib/bot/component'
|
||||
import { BotHandlerDemo } from './lib/bot/handlers/demo'
|
||||
|
||||
if (!process.argv[2]) {
|
||||
throw new Error('Missing parameter: the demobot configuration file path')
|
||||
}
|
||||
const botsConfig = new BotsConfig(process.argv[2])
|
||||
|
||||
const runningBots: ComponentBot[] = []
|
||||
const runningBots: BotComponent[] = []
|
||||
|
||||
async function start (botsConfig: BotsConfig): Promise<void> {
|
||||
await botsConfig.load()
|
||||
@ -19,18 +19,23 @@ async function start (botsConfig: BotsConfig): Promise<void> {
|
||||
logger.info('Starting DemoBot...')
|
||||
|
||||
const config = botsConfig.getDemoBotConfig()
|
||||
const instance = new DemoBot(
|
||||
const instance = new BotComponent(
|
||||
'DemoBot',
|
||||
{
|
||||
service: config.service,
|
||||
domain: config.domain,
|
||||
password: config.password
|
||||
},
|
||||
config.rooms,
|
||||
'DemoBot' // FIXME: handle the case where the nick is already taken.
|
||||
config.mucDomain
|
||||
)
|
||||
runningBots.push(instance)
|
||||
instance.connect().catch(err => { throw err })
|
||||
|
||||
instance.connect().then(async () => {
|
||||
for (const roomId of config.rooms) {
|
||||
const room = await instance.joinRoom(roomId, 'DemoBot')
|
||||
room.attachHandler(new BotHandlerDemo(room))
|
||||
}
|
||||
}).catch(err => { throw err })
|
||||
}
|
||||
if (!atLeastOne) {
|
||||
logger.info('No bot to launch, exiting.')
|
||||
@ -42,7 +47,7 @@ async function shutdown (): Promise<void> {
|
||||
logger.info('Shutdown...')
|
||||
for (const bot of runningBots) {
|
||||
logger.info('Stopping the bot ' + bot.botName + '...')
|
||||
await bot.stop()
|
||||
await bot.disconnect()
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
@ -1,23 +1,20 @@
|
||||
/* eslint-disable no-void */
|
||||
import type { XMPPStanza, XMPPStanzaType } from './types'
|
||||
import type { Node } from '@xmpp/xml'
|
||||
import { logger } from '../logger'
|
||||
import type { XMPPStanza } from './types'
|
||||
import { component, xml, Component } from '@xmpp/component'
|
||||
import type { JID } from '@xmpp/jid'
|
||||
import { component, xml, Component, Options } from '@xmpp/component'
|
||||
import { parse, JID } from '@xmpp/jid'
|
||||
import { BotRoom } from './room'
|
||||
|
||||
interface ComponentConnectionConfig {
|
||||
service: string
|
||||
domain: string
|
||||
password: string
|
||||
}
|
||||
|
||||
abstract class ComponentBot {
|
||||
class BotComponent {
|
||||
protected xmpp?: Component
|
||||
protected address?: JID
|
||||
protected xml = xml
|
||||
public readonly xml = xml
|
||||
protected rooms: Map<string, BotRoom> = new Map()
|
||||
|
||||
constructor (
|
||||
public readonly botName: string,
|
||||
protected readonly connectionConfig: ComponentConnectionConfig
|
||||
protected readonly connectionConfig: Options,
|
||||
protected readonly mucDomain: string
|
||||
) {}
|
||||
|
||||
public async connect (): Promise<void> {
|
||||
@ -36,46 +33,78 @@ abstract class ComponentBot {
|
||||
})
|
||||
|
||||
this.xmpp.on('stanza', (stanza: XMPPStanza) => {
|
||||
logger.debug('stanza received' + (stanza?.toString ? ': ' + stanza.toString() : ''))
|
||||
if (stanza.is('message')) {
|
||||
void this.onMessage(stanza)
|
||||
}
|
||||
if (stanza.is('presence')) {
|
||||
void this.onPresence(stanza)
|
||||
}
|
||||
if (stanza.is('iq')) {
|
||||
void this.onIq(stanza)
|
||||
logger.debug('stanza received' + stanza.toString())
|
||||
if (!stanza.attrs.from) { return }
|
||||
const jid = parse(stanza.attrs.from)
|
||||
const roomJid = jid.bare() // removing the «resource» part of the jid.
|
||||
const room = this.rooms.get(roomJid.toString())
|
||||
if (!room) {
|
||||
return
|
||||
}
|
||||
room.emit('stanza', stanza, jid.getResource())
|
||||
})
|
||||
|
||||
this.xmpp.on('online', (address) => {
|
||||
logger.debug('Online with address' + address.toString())
|
||||
|
||||
this.address = address
|
||||
void this.onOnline()
|
||||
|
||||
// 'online' is emitted at reconnection, so we must reset rooms rosters
|
||||
this.rooms.forEach(room => room.emit('reset'))
|
||||
})
|
||||
|
||||
this.xmpp.on('offline', () => {
|
||||
logger.info(`Stoppping process: ${this.botName} is now offline.`)
|
||||
})
|
||||
|
||||
await this.xmpp.start()
|
||||
}
|
||||
|
||||
public async stop (): Promise<any> {
|
||||
const p = new Promise((resolve) => {
|
||||
this.xmpp?.on('offline', () => {
|
||||
logger.info(`Stoppping process: ${this.botName} is now offline.`)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
public async disconnect (): Promise<any> {
|
||||
for (const [roomId, room] of this.rooms) {
|
||||
logger.debug(`Leaving room ${roomId}...`)
|
||||
await room.part()
|
||||
}
|
||||
await this.xmpp?.stop()
|
||||
await p
|
||||
this.xmpp = undefined
|
||||
}
|
||||
|
||||
protected async onMessage (_stanza: XMPPStanza): Promise<void> {}
|
||||
protected async onIq (_stanza: XMPPStanza): Promise<void> {}
|
||||
protected async onPresence (_stanza: XMPPStanza): Promise<void> {}
|
||||
protected async onOnline (): Promise<void> {}
|
||||
public async sendStanza (
|
||||
type: XMPPStanzaType,
|
||||
attrs: object,
|
||||
...children: Node[]
|
||||
): Promise<void> {
|
||||
attrs = Object.assign({
|
||||
from: this.address?.toString()
|
||||
}, attrs)
|
||||
|
||||
const stanza = this.xml(type, attrs, ...children)
|
||||
logger.debug('stanza to emit: ' + stanza.toString())
|
||||
await this.xmpp?.send(stanza)
|
||||
}
|
||||
|
||||
public async joinRoom (roomId: string, nick: string): Promise<BotRoom> {
|
||||
const roomJID = new JID(roomId, this.mucDomain)
|
||||
const roomJIDstr = roomJID.toString()
|
||||
let room: BotRoom | undefined = this.rooms.get(roomJIDstr)
|
||||
if (!room) {
|
||||
room = new BotRoom(this, roomJID)
|
||||
this.rooms.set(roomJIDstr, room)
|
||||
}
|
||||
await room.join(nick)
|
||||
return room
|
||||
}
|
||||
|
||||
public async partRoom (roomId: string): Promise<void> {
|
||||
const roomJID = new JID(roomId, this.mucDomain)
|
||||
const room = this.rooms.get(roomJID.toString())
|
||||
if (!room) {
|
||||
return
|
||||
}
|
||||
await room.part()
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ComponentConnectionConfig,
|
||||
ComponentBot
|
||||
BotComponent
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { RoomComponentBot } from './room'
|
||||
|
||||
class DemoBot extends RoomComponentBot {
|
||||
protected async onRoomJoin (roomId: string, nick: string): Promise<void> {
|
||||
await this.sendGroupchat(roomId, `Hello ${nick}! I'm the DemoBot, I'm here to demonstrate the chatroom.`)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DemoBot
|
||||
}
|
11
bots/lib/bot/handlers/base.ts
Normal file
11
bots/lib/bot/handlers/base.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { BotRoom } from '../room'
|
||||
|
||||
export abstract class BotHandler {
|
||||
constructor (
|
||||
protected readonly room: BotRoom
|
||||
) {
|
||||
this.init()
|
||||
}
|
||||
|
||||
protected abstract init (): void
|
||||
}
|
19
bots/lib/bot/handlers/demo.ts
Normal file
19
bots/lib/bot/handlers/demo.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { XMPPUser } from '../types'
|
||||
import { BotHandler } from './base'
|
||||
|
||||
export class BotHandlerDemo extends BotHandler {
|
||||
protected init (): void {
|
||||
const room = this.room
|
||||
room.on('room_join', (user: XMPPUser) => {
|
||||
if (user.isMe) {
|
||||
return
|
||||
}
|
||||
if (!room.isOnline()) {
|
||||
return
|
||||
}
|
||||
room.sendGroupchat(
|
||||
`Hello ${user.nick}! I'm the DemoBot, I'm here to demonstrate the chatroom.`
|
||||
).catch(() => {})
|
||||
})
|
||||
}
|
||||
}
|
@ -1,132 +1,130 @@
|
||||
import { ComponentBot, ComponentConnectionConfig } from './component'
|
||||
import { XMPPStanza } from './types'
|
||||
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'
|
||||
|
||||
interface RoomComponentBotRoomDescription {
|
||||
jid: string
|
||||
nick: string
|
||||
users: Map<string, {
|
||||
// TODO: add the current user status somewhere.
|
||||
nick: string
|
||||
}>
|
||||
}
|
||||
export class BotRoom extends EventEmitter {
|
||||
protected state: 'offline' | 'online' = 'offline'
|
||||
protected userJID: JID | undefined
|
||||
protected readonly roster: Map<string, XMPPUser> = new Map()
|
||||
|
||||
abstract class RoomComponentBot extends ComponentBot {
|
||||
protected readonly rooms: {[jid: string]: RoomComponentBotRoomDescription} = {}
|
||||
protected readonly handlers: BotHandler[] = []
|
||||
|
||||
constructor (
|
||||
botName: string,
|
||||
connectionConfig: ComponentConnectionConfig,
|
||||
roomIds: string[],
|
||||
protected readonly nick: string
|
||||
protected readonly component: BotComponent,
|
||||
protected readonly roomJID: JID
|
||||
) {
|
||||
super(botName, connectionConfig)
|
||||
for (const roomId of roomIds) {
|
||||
this.rooms[roomId] = {
|
||||
jid: roomId,
|
||||
nick: nick,
|
||||
users: new Map()
|
||||
}
|
||||
}
|
||||
super()
|
||||
|
||||
this.on('reset', () => {
|
||||
this.state = 'offline'
|
||||
this.roster.clear()
|
||||
})
|
||||
this.on('stanza', (stanza: XMPPStanza, resource?: string) => {
|
||||
this.receiveStanza(stanza, resource)
|
||||
})
|
||||
}
|
||||
|
||||
async onOnline (): Promise<void> {
|
||||
for (const roomId in this.rooms) {
|
||||
const room = this.rooms[roomId]
|
||||
logger.debug(`Connecting to room ${room.jid}...`)
|
||||
const presence = this.xml(
|
||||
'presence',
|
||||
{
|
||||
from: this.address?.toString(),
|
||||
to: room.jid + '/' + room.nick
|
||||
},
|
||||
this.xml('x', {
|
||||
xmlns: 'http://jabber.org/protocol/muc'
|
||||
})
|
||||
)
|
||||
await this.xmpp?.send(presence)
|
||||
}
|
||||
await super.onOnline()
|
||||
public isOnline (): boolean {
|
||||
return this.state === 'online'
|
||||
}
|
||||
|
||||
protected async onPresence (stanza: XMPPStanza): Promise<void> {
|
||||
const [stanzaRoomId, stanzaNick] = stanza.attrs?.from.split('/')
|
||||
if (this.rooms[stanzaRoomId]) {
|
||||
await this.onRoomPresence(stanzaRoomId, stanza, stanzaNick)
|
||||
}
|
||||
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 sendGroupchat (roomId: string, msg: string): Promise<void> {
|
||||
const room = this.rooms[roomId]
|
||||
if (!room) {
|
||||
logger.error('Trying to send a groupchat on an unknown room: ' + roomId)
|
||||
return
|
||||
}
|
||||
const message = this.xml(
|
||||
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: room.jid,
|
||||
from: this.address?.toString()
|
||||
to: this.roomJID.toString()
|
||||
},
|
||||
this.xml('body', {}, msg)
|
||||
this.component.xml('body', {}, msg)
|
||||
)
|
||||
logger.debug('Sending message...: ' + (message.toString() as string))
|
||||
await this.xmpp?.send(message)
|
||||
}
|
||||
|
||||
public async stop (): Promise<any> {
|
||||
for (const roomId in this.rooms) {
|
||||
const room = this.rooms[roomId]
|
||||
logger.debug(`Leaving room ${room.jid}...`)
|
||||
const presence = this.xml(
|
||||
'presence',
|
||||
{
|
||||
from: this.address?.toString(),
|
||||
to: room.jid + '/' + room.nick,
|
||||
type: 'unavailable'
|
||||
}
|
||||
)
|
||||
// FIXME: should wait for a presence stanza from the server.
|
||||
await this.xmpp?.send(presence)
|
||||
public receiveStanza (stanza: XMPPStanza, fromResource?: string): void {
|
||||
if (stanza.name === 'presence') {
|
||||
this.receivePresenceStanza(stanza, fromResource)
|
||||
}
|
||||
await super.stop()
|
||||
}
|
||||
|
||||
protected async onRoomPresence (
|
||||
roomId: string,
|
||||
stanza: XMPPStanza,
|
||||
nick?: string
|
||||
): Promise<any> {
|
||||
const room = this.rooms[roomId]
|
||||
if (!room) {
|
||||
public receivePresenceStanza (stanza: XMPPStanza, fromResource?: string): void {
|
||||
if (!fromResource) {
|
||||
return
|
||||
}
|
||||
if (!nick) {
|
||||
return
|
||||
}
|
||||
const isPresent = stanza.attrs?.type !== 'unavailable'
|
||||
// FIXME: selfPresence should better be tested by searching status=110
|
||||
const selfPresence = room.nick === nick
|
||||
if (!isPresent) {
|
||||
room.users.delete(nick)
|
||||
if (!selfPresence) {
|
||||
await this.onRoomPart(roomId, nick)
|
||||
|
||||
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))
|
||||
}
|
||||
return
|
||||
}
|
||||
room.users.set(nick, {
|
||||
nick
|
||||
})
|
||||
if (!selfPresence) {
|
||||
await this.onRoomJoin(roomId, nick)
|
||||
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 async onRoomJoin (_roomId: string, _nick: string): Promise<void> {}
|
||||
protected async onRoomPart (_roomId: string, _nick: string): Promise<void> {}
|
||||
}
|
||||
|
||||
export {
|
||||
RoomComponentBot
|
||||
public attachHandler (handler: BotHandler): void {
|
||||
this.handlers.push(handler)
|
||||
}
|
||||
}
|
||||
|
@ -5,3 +5,9 @@ export type XMPPStanzaType = 'message' | 'iq' | 'presence'
|
||||
export interface XMPPStanza extends Element {
|
||||
name: XMPPStanzaType
|
||||
}
|
||||
|
||||
export interface XMPPUser {
|
||||
state: 'offline' | 'online'
|
||||
nick: string
|
||||
isMe: boolean
|
||||
}
|
||||
|
@ -32,6 +32,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@xmpp/component": "^0.13.0",
|
||||
"@xmpp/jid": "^0.13.0",
|
||||
"async": "^3.2.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"decache": "^4.6.0",
|
||||
@ -50,6 +51,7 @@
|
||||
"@types/node": "^16.11.6",
|
||||
"@types/winston": "^2.4.4",
|
||||
"@types/xmpp__component": "^0.13.0",
|
||||
"@types/xmpp__jid": "^1.3.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"eslint": "^7.32.0",
|
||||
|
@ -206,7 +206,7 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
|
||||
config.useDemoBot(componentSecret)
|
||||
bots.demobot = demoBotUUIDs
|
||||
demoBotContentObj = JSON.stringify({
|
||||
rooms: demoBotUUIDs.map((uuid) => `${uuid}@room.${prosodyDomain}`),
|
||||
rooms: demoBotUUIDs,
|
||||
service: 'xmpp://127.0.0.1:' + externalComponentsPort,
|
||||
domain: 'demobot.' + prosodyDomain,
|
||||
mucDomain: 'room.' + prosodyDomain,
|
||||
|
Loading…
x
Reference in New Issue
Block a user