Demo Bot: WIP.
This commit is contained in:
82
bots/lib/bot/component.ts
Normal file
82
bots/lib/bot/component.ts
Normal file
@ -0,0 +1,82 @@
|
||||
/* eslint-disable no-void */
|
||||
import { logger } from '../logger'
|
||||
import { XMPP, XMPPXmlFunction, XMPPStanza, XMPPAddress } from './types'
|
||||
|
||||
const { component, xml } = require('@xmpp/component')
|
||||
|
||||
interface ComponentConnectionConfig {
|
||||
service: string
|
||||
domain: string
|
||||
password: string
|
||||
}
|
||||
|
||||
abstract class ComponentBot {
|
||||
protected xmpp?: XMPP
|
||||
protected address?: XMPPAddress
|
||||
|
||||
constructor (
|
||||
public readonly botName: string,
|
||||
protected readonly connectionConfig: ComponentConnectionConfig
|
||||
) {}
|
||||
|
||||
protected xml: XMPPXmlFunction = (...args) => xml(...args)
|
||||
|
||||
public async connect (): Promise<void> {
|
||||
this.xmpp = component({
|
||||
service: this.connectionConfig.service,
|
||||
domain: this.connectionConfig.domain,
|
||||
password: this.connectionConfig.password
|
||||
}) as XMPP
|
||||
|
||||
this.xmpp.on('error', (err: any) => {
|
||||
logger.error(err)
|
||||
})
|
||||
|
||||
this.xmpp.on('offline', () => {
|
||||
logger.info(`${this.botName} is now offline.`)
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
this.xmpp.on('online', (address: XMPPAddress) => {
|
||||
logger.debug('Online with address' + address.toString())
|
||||
|
||||
this.address = address
|
||||
void this.onOnline()
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
await this.xmpp?.stop()
|
||||
await p
|
||||
}
|
||||
|
||||
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> {}
|
||||
}
|
||||
|
||||
export {
|
||||
ComponentConnectionConfig,
|
||||
ComponentBot
|
||||
}
|
11
bots/lib/bot/demobot.ts
Normal file
11
bots/lib/bot/demobot.ts
Normal file
@ -0,0 +1,11 @@
|
||||
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
|
||||
}
|
132
bots/lib/bot/room.ts
Normal file
132
bots/lib/bot/room.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { ComponentBot, ComponentConnectionConfig } from './component'
|
||||
import { XMPPStanza } from './types'
|
||||
import { logger } from '../logger'
|
||||
|
||||
interface RoomComponentBotRoomDescription {
|
||||
jid: string
|
||||
nick: string
|
||||
users: Map<string, {
|
||||
// TODO: add the current user status somewhere.
|
||||
nick: string
|
||||
}>
|
||||
}
|
||||
|
||||
abstract class RoomComponentBot extends ComponentBot {
|
||||
protected readonly rooms: {[jid: string]: RoomComponentBotRoomDescription} = {}
|
||||
|
||||
constructor (
|
||||
botName: string,
|
||||
connectionConfig: ComponentConnectionConfig,
|
||||
roomIds: string[],
|
||||
protected readonly nick: string
|
||||
) {
|
||||
super(botName, connectionConfig)
|
||||
for (const roomId of roomIds) {
|
||||
this.rooms[roomId] = {
|
||||
jid: roomId,
|
||||
nick: nick,
|
||||
users: new Map()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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 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(
|
||||
'message',
|
||||
{
|
||||
type: 'groupchat',
|
||||
to: room.jid,
|
||||
from: this.address?.toString()
|
||||
},
|
||||
this.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)
|
||||
}
|
||||
await super.stop()
|
||||
}
|
||||
|
||||
protected async onRoomPresence (
|
||||
roomId: string,
|
||||
stanza: XMPPStanza,
|
||||
nick?: string
|
||||
): Promise<any> {
|
||||
const room = this.rooms[roomId]
|
||||
if (!room) {
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
room.users.set(nick, {
|
||||
nick
|
||||
})
|
||||
if (!selfPresence) {
|
||||
await this.onRoomJoin(roomId, nick)
|
||||
}
|
||||
}
|
||||
|
||||
protected async onRoomJoin (_roomId: string, _nick: string): Promise<void> {}
|
||||
protected async onRoomPart (_roomId: string, _nick: string): Promise<void> {}
|
||||
}
|
||||
|
||||
export {
|
||||
RoomComponentBot
|
||||
}
|
21
bots/lib/bot/types.ts
Normal file
21
bots/lib/bot/types.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { EventEmitter } from 'events'
|
||||
|
||||
export interface XMPP extends EventEmitter {
|
||||
send: (xml: any) => any
|
||||
start: () => any
|
||||
stop: () => Promise<any>
|
||||
}
|
||||
|
||||
export interface XMPPAddress {
|
||||
toString: () => string
|
||||
}
|
||||
|
||||
export type XMPPStanzaType = 'message' | 'iq' | 'presence'
|
||||
|
||||
export interface XMPPStanza {
|
||||
attrs: any
|
||||
is: (type: XMPPStanzaType) => boolean
|
||||
toString: () => string
|
||||
}
|
||||
|
||||
export type XMPPXmlFunction = (type: string, attrs: object, content?: any) => any
|
Reference in New Issue
Block a user