nubot
Version:
A conversational context-aware chatbot
395 lines (317 loc) • 11.9 kB
JavaScript
'use strict'
const HTTPS = require('https')
const EventEmitter = require('events').EventEmitter
const Adapter = require('../adapter')
const Message = require('../message')
const TextMessage = Message.TextMessage
const EnterMessage = Message.EnterMessage
const LeaveMessage = Message.LeaveMessage
const TopicMessage = Message.TopicMessage
class Campfire extends Adapter {
send (envelope/* , ...strings */) {
const strings = [].slice.call(arguments, 1)
if (strings.length === 0) {
return
}
const string = strings.shift()
if (typeof string === 'function') {
string()
this.send.apply(this, [envelope].concat(strings))
return
}
this.bot.Room(envelope.room).speak(string, (error, data) => {
if (error != null) {
this.robot.logger.error(`Campfire send error: ${error}`)
}
this.send.apply(this, [envelope].concat(strings))
})
}
emote (envelope/* , ...strings */) {
const strings = [].slice.call(arguments, 1)
this.send.apply(this, [envelope].concat(strings.map(str => `*${str}*`)))
}
reply (envelope/* , ...strings */) {
const strings = [].slice.call(arguments, 1)
this.send.apply(this, [envelope].concat(strings.map(str => `${envelope.user.name}: ${str}`)))
}
topic (envelope/* , ...strings */) {
const strings = [].slice.call(arguments, 1)
this.bot.Room(envelope.room).topic(strings.join(' / '), (err, data) => {
if (err != null) {
this.robot.logger.error(`Campfire topic error: ${err}`)
}
})
}
play (envelope/* , ...strings */) {
const strings = [].slice.call(arguments, 1)
this.bot.Room(envelope.room).sound(strings.shift(), (err, data) => {
if (err != null) {
this.robot.logger.error(`Campfire sound error: ${err}`)
}
this.play.apply(this, [envelope].concat(strings))
})
}
locked (envelope/* , ...strings */) {
const strings = [].slice.call(arguments, 1)
if (envelope.message.private) {
this.send.apply(this, [envelope].concat(strings))
}
this.bot.Room(envelope.room).lock(() => {
strings.push(() => {
// campfire won't send messages from just before a room unlock. 3000
// is the 3-second poll.
setTimeout(() => this.bot.Room(envelope.room).unlock(), 3000)
})
this.send.apply(this, [envelope].concat(strings))
})
}
run () {
const self = this
const options = {
token: process.env.HUBOT_CAMPFIRE_TOKEN,
rooms: process.env.HUBOT_CAMPFIRE_ROOMS,
account: process.env.HUBOT_CAMPFIRE_ACCOUNT
}
const bot = new CampfireStreaming(options, this.robot)
function withAuthor (callback) {
return function (id, created, room, user, body) {
bot.User(user, function (_err, userData) {
if (userData.user) {
const author = self.robot.brain.userForId(userData.user.id, userData.user)
const userId = userData.user.id
self.robot.brain.data.users[userId].name = userData.user.name
self.robot.brain.data.users[userId].email_address = userData.user.email_address
author.room = room
return callback(id, created, room, user, body, author)
}
})
}
}
bot.on('TextMessage', withAuthor(function (id, created, room, user, body, author) {
if (bot.info.id !== author.id) {
const message = new TextMessage(author, body, id)
message.private = bot.private[room]
self.receive(message)
}
}))
bot.on('EnterMessage', withAuthor(function (id, created, room, user, body, author) {
if (bot.info.id !== author.id) {
self.receive(new EnterMessage(author, null, id))
}
}))
bot.on('LeaveMessage', withAuthor(function (id, created, room, user, body, author) {
if (bot.info.id !== author.id) {
self.receive(new LeaveMessage(author, null, id))
}
}))
bot.on('TopicChangeMessage', withAuthor(function (id, created, room, user, body, author) {
if (bot.info.id !== author.id) {
self.receive(new TopicMessage(author, body, id))
}
}))
bot.on('LockMessage', withAuthor((id, created, room, user, body, author) => {
bot.private[room] = true
}))
bot.on('UnlockMessage', withAuthor((id, created, room, user, body, author) => {
bot.private[room] = false
}))
bot.Me(function (_err, data) {
bot.info = data.user
bot.name = bot.info.name
return Array.from(bot.rooms).map(roomId => (roomId => bot.Room(roomId).join((_err, callback) => bot.Room(roomId).listen()))(roomId))
})
bot.on('reconnect', roomId => bot.Room(roomId).join((_err, callback) => bot.Room(roomId).listen()))
this.bot = bot
self.emit('connected')
}
}
exports.use = robot => new Campfire(robot)
class CampfireStreaming extends EventEmitter {
constructor (options, robot) {
super()
this.robot = robot
if (options.token == null || options.rooms == null || options.account == null) {
this.robot.logger.error('Not enough parameters provided. I need a token, rooms and account')
process.exit(1)
}
this.token = options.token
this.rooms = options.rooms.split(',')
this.account = options.account
this.host = this.account + '.campfirenow.com'
this.authorization = `Basic ${Buffer.from(`${this.token}:x`).toString('base64')}`
this.private = {}
}
Rooms (callback) {
return this.get('/rooms', callback)
}
User (id, callback) {
return this.get(`/users/${id}`, callback)
}
Me (callback) {
return this.get('/users/me', callback)
}
Room (id) {
const self = this
const logger = this.robot.logger
return {
show (callback) {
return self.get(`/room/${id}`, callback)
},
join (callback) {
return self.post(`/room/${id}/join`, '', callback)
},
leave (callback) {
return self.post(`/room/${id}/leave`, '', callback)
},
lock (callback) {
return self.post(`/room/${id}/lock`, '', callback)
},
unlock (callback) {
return self.post(`/room/${id}/unlock`, '', callback)
},
// say things to this channel on behalf of the token user
paste (text, callback) {
return this.message(text, 'PasteMessage', callback)
},
topic (text, callback) {
const body = { room: { topic: text } }
return self.put(`/room/${id}`, body, callback)
},
sound (text, callback) {
return this.message(text, 'SoundMessage', callback)
},
speak (text, callback) {
const body = { message: { 'body': text } }
return self.post(`/room/${id}/speak`, body, callback)
},
message (text, type, callback) {
const body = { message: { 'body': text, 'type': type } }
return self.post(`/room/${id}/speak`, body, callback)
},
// listen for activity in channels
listen () {
const headers = {
'Host': 'streaming.campfirenow.com',
'Authorization': self.authorization,
'User-Agent': `Hubot/${this.robot != null ? this.robot.version : undefined} (${this.robot != null ? this.robot.name : undefined})`
}
const options = {
'agent': false,
'host': 'streaming.campfirenow.com',
'port': 443,
'path': `/room/${id}/live.json`,
'method': 'GET',
'headers': headers
}
const request = HTTPS.request(options, function (response) {
response.setEncoding('utf8')
let buf = ''
response.on('data', function (chunk) {
if (chunk === ' ') {
// campfire api sends a ' ' heartbeat every 3s
} else if (chunk.match(/^\s*Access Denied/)) {
return logger.error(`Campfire error on room ${id}: ${chunk}`)
} else {
// api uses newline terminated json payloads
// buffer across tcp packets and parse out lines
buf += chunk
return (() => {
let offset
const result = []
while ((offset = buf.indexOf('\r')) > -1) {
let item
const part = buf.substr(0, offset)
buf = buf.substr(offset + 1)
if (part) {
try {
const data = JSON.parse(part)
item = self.emit(data.type, data.id, data.created_at, data.room_id, data.user_id, data.body)
} catch (error) {
item = logger.error(`Campfire data error: ${error}\n${error.stack}`)
}
}
result.push(item)
}
return result
})()
}
})
response.on('end', function () {
logger.error(`Streaming connection closed for room ${id}. :(`)
return setTimeout(() => self.emit('reconnect', id), 5000)
})
return response.on('error', err => logger.error(`Campfire listen response error: ${err}`))
})
request.on('error', err => logger.error(`Campfire listen request error: ${err}`))
return request.end()
}
}
}
get (path, callback) {
return this.request('GET', path, null, callback)
}
post (path, body, callback) {
return this.request('POST', path, body, callback)
}
put (path, body, callback) {
return this.request('PUT', path, body, callback)
}
request (method, path, body, callback) {
const logger = this.robot.logger
const headers = {
'Authorization': this.authorization,
'Host': this.host,
'Content-Type': 'application/json',
'User-Agent': `Hubot/${this.robot != null ? this.robot.version : undefined} (${this.robot != null ? this.robot.name : undefined})`
}
const options = {
'agent': false,
'host': this.host,
'port': 443,
'path': path,
'method': method,
'headers': headers
}
if (method === 'POST' || method === 'PUT') {
if (typeof body !== 'string') {
body = JSON.stringify(body)
}
body = Buffer.from(body)
options.headers['Content-Length'] = body.length
}
const request = HTTPS.request(options, function (response) {
let data = ''
response.on('data', chunk => {
data += chunk
})
response.on('end', function () {
if (response.statusCode >= 400) {
switch (response.statusCode) {
case 401:
throw new Error('Invalid access token provided')
default:
logger.error(`Campfire HTTPS status code: ${response.statusCode}`)
logger.error(`Campfire HTTPS response data: ${data}`)
}
}
if (callback) {
try {
return callback(null, JSON.parse(data))
} catch (_err) {
return callback(null, data || {})
}
}
})
return response.on('error', function (err) {
logger.error(`Campfire HTTPS response error: ${err}`)
return callback(err, {})
})
})
if (method === 'POST' || method === 'PUT') {
request.end(body, 'binary')
} else {
request.end()
}
return request.on('error', err => logger.error(`Campfire request error: ${err}`))
}
}