Git Product home page Git Product logo

coffea's Introduction

coffea

NPM version Dependencies

coffea lays the foundations you need to painlessly and effortlessly connect to multiple chat protocols

Not maintained: coffea is currently not maintained anymore due to lack of time. If you are interested in continuing this project feel free to contact me at [email protected]

Attention: beta15 changed event.channel to event.chat for more consistency across protocols. Furthermore, helper functions now accept only one argument with all the options for building the event. Make sure to update your code when upgrading! You can also use the improved reply function now (which defaults to the current chat if chat is not supplied):

reply('hello world!') // reply with a simple message
reply(message('hello world!')) // or you can do this
reply(message({ // reply with a more complex message
  text: 'hello world!',
  protocolSpecificOption: 'something'
}))

Table of contents:

Quickstart

Use the coffea-starter project to quickly get started developing with coffea!

Installation

You can install the latest coffea version like this:

npm install --save [email protected]

As for protocols, we're working on coffea-irc, coffea-slack and coffea-telegram. Feel free to build your own if you want to play around with coffea.

Connecting

The coffea core exposes a connect function (along with other functions, which are explained later). It can be imported like this:

import { connect } from 'coffea'

This function loads the required protocols (via node_modules) and returns an instance container, which has the on and send functions.

// create an instance container for one irc instance
// Note: options get passed to the protocol, which handles them (e.g. autojoin channels on irc)
//       please refer to the protocol documentation for more information about these options
//       (usually available at https://npmjs.com/package/coffea-PROTOCOLNAME)
const networks = connect([
  {
    protocol: 'irc',
    network: '...',
    channels: ['#foo', '#bar']
  }
])

// the instance container exposes the `on` and `send` functions:
networks.send({...}) // we'll learn about sending events later
networks.on('message', (msg, reply) => {...}) // we'll learn about listening to events later

Note: You need to install coffea-PROTOCOLNAME to use that protocol, e.g. npm install coffea-slack

You can now use this function to connect to networks and create instance containers! 🎉

Events

Events are the central concept in coffea. They have a certain structure (object with a type key):

{
  type: 'EVENT_NAME',
  ...
}

For a message, it could look like this (imagine a git bot):

{
  type: 'message',
  chat: '#dev',
  text: 'New commit!'
}

Note: In coffea, outgoing and ingoing events are always consistent - they look the same. That way you don't need to memorize two separate structures for sending/receiving events - awesome! (might even save some code)

Listening on events

coffea's connect function transforms the passed configuration array into an instance container, which is an enhanced array. This means you can use normal array functions, like map and filter. e.g. you could filter networks and only listen to slack networks, or you could use map to send a message to all networks. You could even combine them!

// only listen to `slack` networks:
networks.filter(network => network.protocol === 'slack')

// `map` and `filter` combined
networks
  .filter(network => network.protocol === 'slack')
  .map(network => console.log(network))

The array is enhanced with an on function (and a send function, more on that later), which allows you to listen to events on the instance container:

networks.on('event', (event, reply) => { ... })

networks
  .filter(network => network.protocol === 'slack')
  .on('message', msg => console.log(msg.text))

// sending events will be explained more later
const parrot = (msg, reply) => reply(msg.text)
networks.on('message', parrot)

Event helpers

You probably don't want to deal with raw event objects all the time - you write a lot of boilerplate and it's prone to error. That's why coffea (and the protocols) provide helper functions that create events, they can be imported like this:

// `message` is a core event helper (it works on all protocols)
import { message } from 'coffea'

// `attachment` is a protocol specific event helper (it only works on certain protocols)
import { attachment } from 'coffea-slack'

Note: Protocols should try to keep similar functionality consistent (e.g. if two protocols support attachments, keep the api consistent so you can use either helper function and it will work for both protocols).

Now you can create an event like this:

message(name, chat, options)
message('New commit!', '#dev', { protocolSpecificOption: 'something' })

Or you can use an object instead:

message({
  text: 'New commit!',
  chat: '#dev',
  protocolSpecificOption: 'something'
})

The structure for event helpers is:

eventName(argument1, argument2, ..., options) // for global events
eventName(argument1, argument2, ..., chat, options) // for events that are specific to a certain chat

Make sure your event helper is also usable with an object:

eventName({ argument1, argument2, ..., option1, option2, ...})
eventName({ argument1, argument2, ..., chat, option1, option2, ...})

(eventName should always equal the type of the event that is returned to avoid confusion!)

Multiple protocols can expose the same helper functions, but with enhanced functionality. e.g. for Slack you could do:

import { message, attachment } from 'coffea-slack'
message({ chat, text, attachment: attachment('test.png') })

Note: coffea core's message helper function (if you import with import { message } from 'coffea') does not implement the attachment option!

Core events

coffea defines certain event helpers that should be used when developing protocols in order to ensure consistency. You can import and use all helpers like this:

import { event, connection, message, privatemessage, command, error } from 'coffea'
event(name, data) // `name` required; e.g. event('ping')
connection()
message(text, chat, options) // `text` required; e.g. message('hi!')
privatemessage(text, chat, options) // `text` required; e.g. privatemessage('hi!')
command(cmd, args, chat, options) // `cmd` required; e.g. `command('ping')`
error(err, options) // `err` required; e.g. `error(new Error('fail'))`

You can alternatively pass an object as the first parameter instead, e.g.:

message({
  text: 'hello world',
  someOption: true // instead of using `options` as a separate argument, you can just pass them directly in the object
})

Example: Writing an event helper

import { isObject } from 'coffea'

/**
 * Make sure to add some information about the event helper here.
 *
 * @param  {type} arg
 * @param  {type} [optionalArg]
 * @return {Object} example event
 */
export const example = (arg, optionalArg, options) => {
  // make the helper work with an object
  if (isObject(arg)) {
    // we need to rename `arg` to `_arg` here to avoid overshadowing the variable
    let { arg: _arg, optionalArg, ...options } = arg
    return example(_arg, optionalArg, options)
  }

  // do some sanity checks
  if (!arg) {
    throw new Error(
      'An `example` event needs at least a `arg` parameter, ' +
      'e.g. example(\'arg\') or example({ arg: \'arg\' })'
    )
  }

  // create the event
  // make sure to put ...options first or it will overwrite other properties!
  return {
    ...options,
    type: 'example',
    arg, optionalArg
  }
}

Sending events

Now that you know how to create events, let's send them to the networks.

The instance container is also enhanced with a send function, which allows you to send calling events to the networks. e.g. sending a calling message event will send a message to the network.

Note: As mentioned before, in coffea calling events and receiving events always look the same.

You can use the message helper function to send an event to all networks:

import { message } from 'coffea'

// send to all networks:
networks.send(message({ chat: '#dev', text: 'Commit!' }))

// send to slack networks only:
networks
  .filter(network => network.protocol === 'slack')
  .send(message({ chat: '#random', text: 'Secret slack-only stuff.' }))

send in combination with on

If you're sending events as a response to another event, you should use the reply function that gets passed as an argument to the listener. It will automatically figure out where to send the message instead of sending it to all networks (like networks.send does):

networks.on('message', (msg, reply) => reply(msg.text))

You may want to keep the function definitions (const parrot = ...) separate from the on statement (networks.on(...)). This allows for easy unit tests:

// somefile.js
export const parrot = (msg, reply) => reply(msg.text)
export const reverse = (msg, reply) => {
  const reversedText = msg.text.split('').reverse().join('')
  reply(reversedText)
}

// unittests.js
import { assert } from 'my-favorite-testing-library'
import { parrot, reverse } from './somefile'
parrot('hello world', (msg) => assert(msg.text === 'hello world'))
reverse('hello world', (msg) => assert(msg.text === 'dlrow olleh'))

// main.js
import connect from 'coffea'
import { parrot, reverse } from './somefile'
const networks = connect([...]) // put network config here
networks.on('message', reverse)
// or...
networks.on('message', parrot)

Example: Reverse bot

import { connect, message } from 'coffea'

const networks = connect([
  {
    protocol: 'irc',
    network: '...',
    channels: ['#foo', '#bar']
  },
  {
    protocol: 'telegram',
    token: '...'
  },
  {
    protocol: 'slack',
    token: '...'
  }
])

const reverse = (msg, reply) => {
  const reversedText = msg.text.split('').reverse().join('')
  reply(reversedText)
}

networks.on('connection', (evt) => console.log('connected to ' + evt.network))

networks.on('message', reverse)

Protocols

This is a guide on how to implement a new protocol with coffea.

Protocols are functions that take config (a network configuration), and a dispatch function as arguments. They return a function that will handle all calling events sent to the protocol later.

A simple protocol could look like this:

export default const dummyProtocol = (config, dispatch) => {
  // mock connect event
  dispatch({
    type: 'connect',
    network: config.network
  })

  return event => {
    switch (event.type) {
      case 'message':
        dispatch({
          type: 'message',
          text: event.text
        })
        break
      default:
        dispatch({
          type: 'error',
          text: 'Unknown event'
        })
        break
    }
  }
}

To use this protocol, you have to pass the protocol function to connect:

import { connect, message } from 'coffea'
import dummyProtocol from './dummy'

const networks = connect([
  {
    protocol: dummyProtocol,
    network: 'test'
  }
])

const logListener = msg => console.log(msg)
networks.on('message', logListener)

networks.send(message('hello world!'))

dummyProtocol's event handler will then receive the following as the event argument:

{
  type: 'message',
  text: 'hello world!'
}

Which means it will dispatch the message event, which results in the on('message', listener) listeners getting called with the same event argument.

Finally, the logListener function will get called, which results in the following output on the console:

{
  type: 'message',
  text: 'hello world!'
}

forward helper

forward({
  eventName: function,
  ...
})

You probably don't want to use switch statements to parse the events, which is why coffea provides a forward helper function. It forwards the events (depending on their type) to the specific handler function and can be used like this:

import { forward } from 'coffea'

export default const dummyProtocol = (config, dispatch) => {
  // mock connect event
  dispatch({
    type: 'connect',
    network: config.network
  })

  return forward({
    'message': event => dispatch({
        type: 'message',
        text: event.text
      }),

    'default': event => dispatch({
        type: 'error',
        text: 'Unknown event'
      })
  })
}

Note: 'default' will be called if the event doesn't match any of the other defined types.

This helper also allows you to separate your event handlers from the protocol logic:

import { forward } from 'coffea'

const messageHandler = dispatch => event =>
  dispatch({
    type: 'message',
    text: event.text
  })

const defaultHandler = dispatch => event =>
  dispatch({
    type: 'error',
    err: new Error('Unknown event')
  })

export default const dummyProtocol = (config, dispatch) => {
  // mock connect event
  dispatch({
    type: 'connect',
    network: config.network
  })

  return forward({
    'message': messageHandler(dispatch),
    'default': defaultHandler(dispatch)
  })
}

You can (and should!) use the same coffea helpers for protocols:

import { forward, message, error } from 'coffea'

const messageHandler = dispatch => event =>
  dispatch(message({ text: event.text })

const defaultHandler = dispatch => event =>
  dispatch(error({ err: new Error('Unknown event') })

export default const dummyProtocol = (config, dispatch) => {
  // mock connect event
  dispatch({
    type: 'connect',
    network: config.network
  })

  return forward({
    'message': messageHandler(dispatch),
    'default': defaultHandler(dispatch)
  })
}

coffea's People

Contributors

cmilhench avatar erming avatar greenkeeperio-bot avatar kramerc avatar omnidan avatar tj avatar tmcw avatar whiskers75 avatar zackp30 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

coffea's Issues

write unit tests for multiple networks

  • away.js
  • channel.js
  • invite.js
  • join.js
  • kick.js
  • mode.js
  • motd.js
  • names.js
  • nick.js
  • notice.js
  • part.js
  • ping.js
  • privmsg.js
  • quit.js
  • server.js
  • topic.js
  • user.js
  • welcome.js
  • whois.js

throttling messages

throttle our messages going outwards so we don't spam or have our messages throttled by the server itself.

Soon™ merger todolist

List of stuff that soon™ implements but coffea doesn't:

IRCv3.1

  • capability negotiation
  • account-notify extension (optional)
  • away-notify extension (optional)
  • extended-join extension (optional)
  • multi-prefix extension
  • sasl extension (only PLAIN)

IRCv3.2

  • monitor extension
  • message tags
  • account-tag extension

Replace "*" with explicit ranges.

Can you look into replacing "*" dependencies with explicit ranges. Right now anyone installing coffea will get future version updates to dependencies, which may be incompatible with the API used here.

The tool npm-explicit-deps would help you in keeping them straight. I would also protect you against future changes to "^" in npm version 2.

IRCv3 support

SSL support

ssl: true in the config should actually connect to the network with ssl (and set default port to 6697)

easy irc colors

we should be able to easily set irc colors, like client.send(client.color(fg, bg), 'test', client.color('reset')) or even [red] or so.

add functions to event

event.send(event.user, 'hi') would resolve to client.send(event.user, 'hi', event.network)

plugin manager for coffea

plugin manager class that let's you .load and .unload plugins, store plugin info, get version from plugin (if available), get a list of loaded plugins, etc... later: dependency management, priorities (for now alphabetically sorted), etc... and you should be able to .loadDir too which would be recursive #37 , e.g. .loadDir('lib/plugins/')

allow loading plugins during runtime, then make a small project called coffeebot that utilizes this and loads plugins from a specific directory

Should support reconnection

coffea should detect the end of the stream and have support for reconnecting back to the same network with the same options (and preferably with some sort of exponential back-off)

Event objects? Event objects!

Let's make events objects and have things like event.reply('test') to instantly reply to a message. Resolves to client.send(event.channel ? event.channel : event.user, 'test', event.network)

new config format

Single server (required params):

var client = coffea({
  host: 'localhost',
  nick: 'test'
});

Single server (optional params):

var client = coffea({
  name: 'testnet', // default assigned by useStream
  host: 'localhost',
  nick: 'test',
  port: 6698, // default 6667, on ssl 6697
  ssl: true, // default false
  username: 'test', // default equals nick
  realname: 'test', // default equals nick
});

For multiple networks, pass an array of network objects. You can then differentiate networks by name. If a name isn't specified, an id is assigned to the network. First network will be 0, second network 1, etc.

Coffea then parses the network object, creates the stream and runs client.useStream(stream, network.name). Then allow user to add networks during runtime and return the stream_id (returned by client.useStream).

By default, all listeners and commands apply to all networks. You can send a command to a specific network only by passing the stream_id (network name). In listeners, the active stream_id is available in event.network. To simplifier listeners for specific networks, we could do:

client.on('testnet:message', function (event) {
});

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.