-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
759 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
const createClient = require('./chat-client'); | ||
const md5 = require('md5'); | ||
const {v4: createId} = require('uuid'); | ||
|
||
const getAvatar = (email) => | ||
`http://www.gravatar.com/avatar/${md5(email)}?s=48&d=identicon&r=PG`; | ||
|
||
const createBotUser = (name) => { | ||
const id = createId(); | ||
const email = `bot_${id}@chatbot.com`; | ||
const avatar = getAvatar(email); | ||
return { | ||
name, | ||
id, | ||
email, | ||
avatar, | ||
fullName: name, | ||
familyName: name, | ||
}; | ||
}; | ||
|
||
const createBot = (name) => { | ||
const botUser = createBotUser(name); | ||
const client = createClient('BOT/' + encodeURIComponent(JSON.stringify(botUser))); | ||
|
||
const sendMessage = (...args) => | ||
setTimeout(() => client.sendMessage(...args), 200); | ||
|
||
const respondWith = (getResponse) => (action, ...rest) => { | ||
const text = typeof getResponse === 'function' | ||
? getResponse(action, ...rest) | ||
: getResponse; | ||
const {receiver, sender} = action; | ||
sendMessage(text, receiver === botUser.id ? sender : receiver); | ||
}; | ||
|
||
const whenOneToOne = (fn) => (action, ...rest) => { | ||
if (action.receiver === botUser.id) { | ||
fn(action, ...rest); | ||
} | ||
}; | ||
|
||
return { | ||
data: botUser, | ||
init: client.init, | ||
respondWith, | ||
whenOneToOne, | ||
matches, | ||
sendMessage, | ||
on: client.on, | ||
}; | ||
}; | ||
|
||
const escapeRegExp = (string) => | ||
string.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); | ||
|
||
const textMatchesSelector = (text, selector) => { | ||
const matches = text.startsWith(selector); | ||
const parsedText = matches | ||
? text.replace(new RegExp(escapeRegExp(selector) + '\s*'), '') | ||
: null; | ||
return [matches, parsedText]; | ||
}; | ||
|
||
const matches = (selectors) => (action) => { | ||
const handled = Object.keys(selectors) | ||
.filter(selector => selector !== 'default') | ||
.some(selector => { | ||
const {text} = action.payload; | ||
const [matches, parsedText] = textMatchesSelector(text, selector); | ||
if (matches) { | ||
selectors[selector](action, parsedText); | ||
} | ||
return matches; | ||
}); | ||
|
||
if (!handled && 'default' in selectors) { | ||
selectors['default'](action); | ||
} | ||
}; | ||
|
||
module.exports = { | ||
createBot, | ||
matches, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
const {createBot} = require('../bot-api'); | ||
|
||
const createEchoBot = (name) => { | ||
const bot = createBot(name); | ||
const {whenOneToOne, respondWith} = bot; | ||
bot.on('message', whenOneToOne(respondWith(({payload}) => payload.text))); | ||
return bot; | ||
}; | ||
|
||
module.exports = createEchoBot; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
const {createBot, matches} = require('../bot-api'); | ||
|
||
const createTodoBot = (name) => { | ||
const todos = {}; | ||
const bot = createBot(name); | ||
const {whenOneToOne, respondWith} = bot; | ||
|
||
const printTodos = todosForUser => | ||
todosForUser.map((todo, idx) => `${idx + 1}. ${todo}`).join(',\n'); | ||
|
||
bot.on('message', matches({ | ||
|
||
'/todos': respondWith(({sender}) => { | ||
const todosForUser = todos[sender]; | ||
if (todosForUser) { | ||
return 'These are your todos: \n' + printTodos(todosForUser); | ||
} else { | ||
return "You don't have pending todos!"; | ||
} | ||
}), | ||
|
||
'/todo': respondWith(({sender}, newTodo) => { | ||
const todosForUser = todos[sender] || []; | ||
todos[sender] = [...todosForUser, newTodo] | ||
return `New todo saved: "${newTodo}" \nPending: \n${printTodos(todos[sender])}`; | ||
}), | ||
|
||
'/done': respondWith(({sender}, idx) => { | ||
const doneTodoIdx = parseInt(idx) - 1; | ||
const todosForUser = todos[sender] || []; | ||
const doneTodo = todosForUser[doneTodoIdx]; | ||
const pendingTodos = todosForUser.filter((todo, idx) => idx !== doneTodoIdx); | ||
todos[sender] = pendingTodos; | ||
return doneTodo | ||
? `"${doneTodo}" marked as done \nPending: \n${printTodos(pendingTodos)}` | ||
: todosForUser.length | ||
? `You only have ${todosForUser.length} todos` | ||
: "You don't have pending todos!"; | ||
}), | ||
|
||
'/help': respondWith(() => [ | ||
'/todos lists the todos', | ||
'/todo <whatever> adds a new todo', | ||
'/done <number> marks a todo as done' | ||
].join(',\n')), | ||
|
||
default: whenOneToOne(respondWith('Type /help')), | ||
})); | ||
|
||
return bot; | ||
}; | ||
|
||
module.exports = createTodoBot; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
const WebSocket = require('ws'); | ||
|
||
const CONNECTING = 0; | ||
const OPEN = 1; | ||
|
||
const dev = true; | ||
const PRODUCTION_CHAT_SERVER = 'wss://react-chat-server.herokuapp.com'; | ||
const DEV_CHAT_SERVER = 'ws://localhost:8080'; | ||
const chatServerUrl = dev ? DEV_CHAT_SERVER : PRODUCTION_CHAT_SERVER; | ||
|
||
const createClient = (token) => { | ||
let ws; | ||
const listeners = {}; | ||
|
||
const handleMessage = m => { | ||
const action = JSON.parse(m.data); | ||
(listeners[action.type] || []).forEach(l => l(action)); | ||
}; | ||
|
||
return { | ||
/** | ||
* Inits the chat connection with the server | ||
* @param {string} token the google auth session token | ||
*/ | ||
init() { | ||
if (ws) { | ||
throw new Error('Chat client already initialized'); | ||
} | ||
ws = new WebSocket(`${chatServerUrl}/${token}`); | ||
ws.addEventListener('message', handleMessage); | ||
}, | ||
|
||
/** | ||
* Fetch the users list from server. | ||
* @return {Promise} resolves to an array of users: | ||
* { | ||
* fullName: string, | ||
* avatar: string, | ||
* name: string, | ||
* familyName: string, | ||
* email: string, | ||
* id: string | ||
* } | ||
*/ | ||
getUsers() { | ||
this.send({type: 'getUsers', receiver: 'server'}); | ||
}, | ||
|
||
/** | ||
* Sends a chat message to a recipient | ||
* @param {string} messageText | ||
* @param {string} receiver userId of the receiver. When not provided, | ||
* the message is sent to all the users | ||
*/ | ||
sendMessage(messageText, receiver = 'all') { | ||
this.send({type: 'message', receiver, payload: {text: messageText}}); | ||
}, | ||
|
||
/** | ||
* Sends an action to the chat server | ||
* @param {object} action an action has the following form: | ||
* { | ||
* type: string, // for example 'message' | ||
* receiver: string, // the userId of the recipient or 'all' | ||
* [sender]: string, // not needed, the server knows you | ||
* [time]: number, // automatically set to current timestamp. | ||
* payload: object, // any data associated with the event | ||
* } | ||
* @return {[type]} [description] | ||
*/ | ||
send(action) { | ||
action.time = action.time || Date.now(); | ||
if (ws.readyState === CONNECTING) { | ||
setTimeout(() => this.send(action), 100); | ||
} else if (ws.readyState === OPEN) { | ||
ws.send(JSON.stringify(action)); | ||
} | ||
}, | ||
|
||
/** | ||
* Registers a listener for a given event type | ||
* @param {string} event | ||
* @param {Function} listener will be called with every | ||
* chat event of the specified type received from the sever | ||
* @return {Function} dettach function to remove the event | ||
* listener. | ||
*/ | ||
on(event, listener) { | ||
if (event in listeners) { | ||
listeners[event].push(listener); | ||
} else { | ||
listeners[event] = [listener]; | ||
} | ||
return () => this.off(event, listener); | ||
}, | ||
|
||
/** | ||
* Dettached a previously registered listener | ||
* @param {string} event the event type | ||
* @param {Function} listenerToRemove | ||
*/ | ||
off(event, listenerToRemove) { | ||
if (event in listeners) { | ||
listeners[event] = listeners[event].filter(l => l !== listenerToRemove); | ||
} | ||
}, | ||
}; | ||
}; | ||
|
||
module.exports = createClient; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.