Plugin Authoring Guide
Adapters
An adapter is a plugin that allows Lita to connect to a particular chat service. It's a Ruby class that inherits from Lita::Adapter
and implements a set of required methods. In this section of the guide, we'll walk through the process of creating a plugin for the fictitious chat service, FancyChat.
Generating an adapter
Start by generating the files for a new plugin by running lita adapter NAME_OF_YOUR_ADAPTER. In this case, the name would be fancychat. You can type either fancychat or lita-fancychat. If you leave off the prefix, Lita will add it for you.
The generator command creates a new directory called lita-fancychat
with all the files required for a Ruby gem. The adapter class will be defined in the file lib/lita/adapters/fancychat.rb
and within the Lita::Adapters
namespace. It's convention for Lita adapters to be in this namespace, but it's not strictly necessary. Any class in any location can serve as an adapter. Notice that the adapter is a class that inherits from Lita::Adapter
and that the adapter is registered with Lita using the call Lita.register_adapter(:fancychat, FancyChat)
. The first argument is a Ruby symbol which people will use to configure their Lita instances to use your adapter.
The generator also creates a test file for the adapter at spec/lita/adapters/fancychat_spec.rb
. Testing is covered later in the guide.
From here on out, it's your job to implement the required methods to make the adapter function!
Configuration
Adapters will likely require some configuration in order to connect to the chat service, such as a username and password. To set up this configuration, use the class-level config
method, which defines configuration attributes for the adapter. These attributes will be exposed to users on the Lita.config.adapters.fancychat
object and they'll set values for them in their lita_config.rb
file. Let's say Fancychat requires only two attributes to make a connection: username and password. We'll also add one optional attribute that will hold the names of the channels (chat rooms) Lita should join after connecting.
module Lita
module Adapters
class Fancychat < Adapter
config :username, type: String, required: true
config :password, type: String, required: true
config :channels, type: Array
end
Lita.register_adapter(:fancychat, Fancychat)
end
end
A user would then configure your adapter like this:
Lita.configure do |config|
config.robot.adapter = :fancychat
config.adapters.fancychat.username = "litabot"
config.adapters.fancychat.password = "#n9sd90cs@MKfs"
config.adapters.fancychat.channels = ["#general", "#engineering"]
end
The config
method takes the name of the attribute to create as a Ruby symbol, with a few optional parameters. For full details on using the config
method, take a look at the configuration page.
Required methods
With configuration out of the way, it's time to implement the required methods for the adapter to actually function. The abstract methods adapters should implement are: run, shut_down, send_messages, set_topic, join, and part.
In some cases, some of these methods may not be applicable to a particular chat service, in which case you can just leave them unimplemented, and plugins that call them will simply cause a warning to Lita's log saying the adapter doesn't implement the method.
run and shut_down
run
is the most important method. This is what gets called by Lita when starting up and must establish a connection with the chat service and begin listening for incoming messages. The mechanism by which you do this is entirely up to you, but it's important that you implement some concurrency mechanism (threads, EventMachine, Celluloid, etc.) so that incoming messages can still be processed even while a previous message is still being processed. The run
method itself should block, since Lita itself doesn't do anything to keep the robot running.
Imagine that Fancychat has its own client library for Ruby that handles concurrency. We can use the client to implement the adapter methods, beginning with run (some boilerplate code removed for brevity):
require 'fancychat'
class Fancychat < Adapter
def initialize(robot)
super
@client = ::Fancychat::Client.new(config.username, config.password)
end
def run
@client.on_connect do
robot.trigger(:connected)
config.rooms.each { |room| @client.join(room) }
end
@client.on_message do |message, user, channel|
user = Lita::User.find_by_name(user)
user = Lita::User.create(user) unless user
source = Lita::Source.new(user: user, room: channel)
message = Lita::Message.new(robot, message, source)
robot.receive(message)
end
@client.connect
end
def shut_down
@client.disconnect
end
end
That's a lot of code! Let's walk through what's happening here:
The Fancychat client library is going to do the heavy lifting for us, so we require it at the top of the file. Because all of the methods in the adapter will need to interface with the Fancychat service in some way, we override the constructor (initialize
) to create an instance of the Fancychat client library with a username and password. The config
method is a convenience method to access Lita.config.adapters.fancychat
, so we can easily get at the values the user set for the configuration attributes we defined.
The run
method has three responsibilites: Set up a callback to perform some one-time tasks when the connection to Fancychat is established, set up the logic for what to do when a message is received over Fancychat, and finally, connect to Fancychat.
The Fancychat client library provides an on_connect
method which takes a block and will execute the block as soon as the connection is established. We want two things to happen at this point: we want to trigger a :connected
event to the entire Lita system (full details about this can be found on the events page), and we want Lita to join each of the chat rooms the user specified with the rooms
configuration attribute.
The Fancychat client library also provides an on_message
method which allows us to control what will happen when an message is received. The method yields the raw string message, the string username of the person who sent the message, and the string name of the channel it was sent in, if applicable. To dispatch this message to Lita's handler system, we must construct a few objects.
Lita keeps track of users using the Lita::User
class and automatically saves them in Redis. Most of the time when Lita receives a message, it's from a user Lita has seen before, so we use Lita::User.find_by_name
to try to find an existing user with that name. If one isn't found, we use the create
method to initialize and save a new one. In either case, we now have a Lita user object.
Next, we construct a Lita::Source
object, which represents the person or room where a message originated. Messages will usually always have a user associated with them, whereas the room/channel may be optional if the message was sent directly to Lita privately.
Next, we create a Lita::Message
object to represent the actual message. This contains a reference to the currently running Lita::Robot
object, and the message and source objects we just created.
Finally, we pass the message object to the currently running robot using the receive
method. Lita will take everything from there.
Now that we've set up the behavior for connection and incoming message handling, all run needs to do is actually connect, which is handled by the client library's connect
method. This will block until it receives a method call telling it to disconnect or the Lita process is sent a signal to interrupt/terminate.
shut_down
is called automatically when the Lita process is killed. Use it to perform any last minute clean up that might be necessary. In this case, we just call the Fancychat client library's disconnect
method for a clean shut down. In many cases, the body of the shut_down
method can be empty, because no clean up is required.
Other methods
The other methods adapters implement are generally not as complicated as the run
method. The most important remaining method is send_messages
, which is the interface other plugins use to send messages out from Lita back to users and chat rooms.
class Fancychat < Adapter
def send_messages(target, messages)
messages.each do |message|
@client.send(target.room || target.user, message)
end
end
end
send_messages
receives a target, which is a Lita::Source
object designating the desired recipient of the message, and messages, an array of strings to send to the target. In this case, we simply call the Fancychat client library's send
method with each message, and it handles the details of the network communication.
Setting a chat room's topic message, and making Lita join and part chat rooms on demand are the last bits of functionality adapters can provide. These are generally similar to sending messages, in the sense that you will probably just be delegating to a client library for the chat service, which is what we will do here with Fancychat:
class FancyChat < Adapter
def set_topic(target, topic)
@client.set_topic(target.source, topic)
end
def join(room_id)
@client.join(room_id)
end
def part(room_id)
@client.part(room_id)
end
end
The body of these methods are self-explanatory. With that, our adapter is finished. Let's take a look at it all together:
require 'fancychat'
module Lita
module Adapters
class Fancychat < Adapter
config :username, type: String, required: true
config :password, type: String, required: true
config :channels, type: Array
def initialize(robot)
super
@client = ::Fancychat::Client.new(config.username, config.password)
end
def run
@client.on_connect do
robot.trigger(:connected)
config.rooms.each { |room| @client.join(room) }
end
@client.on_message do |message, user, channel|
user = Lita::User.find_by_name(user)
user = Lita::User.create(user) unless user
source = Lita::Source.new(user: user, room: channel)
message = Lita::Message.new(robot, message, source)
robot.receive(message)
end
@client.connect
end
def send_messages(target, messages)
messages.each do |message|
@client.send(target.room || target.user, message)
end
end
def shut_down
@client.disconnect
end
def set_topic(target, topic)
@client.set_topic(target.source, topic)
end
def join(room_id)
@client.join(room_id)
end
def part(room_id)
@client.part(room_id)
end
end
Lita.register_adapter(:fancychat, Fancychat)
end
end
Helper methods
Adapters have access to the following helper instance methods:
Name | Description |
---|---|
robot |
Direct access to the currently running Lita::Robot object. |
translate (aliased to t ) |
A convenience method for easily localizing text. Takes the string key of the translation, and an optional hash of values to interpolate into the translated string. The same method is available at the class level as well. |
config |
The adapter's namespaced configuration object. Equivalent to robot.config.adapters.your_adapter_namespace . |
log |
A convenience method for accessing the global logger object. Equivalent to Lita.logger . |
Chat-service-specific methods
The Lita::Robot
API has methods to cover the lowest common denominator of functionality supported across many different chat services. If you are building an adapter for a chat service that has advanced or non-standard functionality (e.g. attachments on Slack), you should implement Lita::Adapter#chat_service
. This method should return an object of your choice with methods for accessing these chat-service-specific methods. Plugin authors can access this object by calling Lita::Robot#chat_service
. Be sure to document any custom methods for your adapter exposed through this interface so users of your adapter know what features are available to them.
Block syntax
If you're just playing around, and don't want to deal with all the boilerplate of a Ruby gem, adapters can also be created by passing a block to Lita.register_adapter
:
Lita.register_adapter(:fancychat) do
config :username, type: String, required: true
config :password, type: String, required: true
config :channels, type: Array
def initialize(robot)
super
@client = ::Fancychat::Client.new(config.username, config.password)
end
# etc...
end
This has the same effect as defining a named class and registering it manually, except that your adapter will be an anonymous class only stored internally in Lita's registry.
For more detailed examples of adapters, check out the built in shell adapter, lita-hipchat, or lita-irc. Refer to Lita's API documentation for the exact API adapters must implement.