Creating a custom responder
Buffy will load and make available any responder that is located in the app/responders
directory. The simplest way to organize your responders is to add them in a subfolder inside the responders
dir, defining a module for the custom responders.
During this guide as an example, we’ll create a simple responder to get the time.
Responder structure
A responder is a ruby class containing five elements:
keyname: the handle for the responder in the configuration file
define_listening method: a place to declare what events the responder is listening to
process_message method: the code to perform whatever the responder does
description method: to add a short description of the responder for documenting purposes
example_invocation method: to show users how to invoke the responder
The Responder Ruby class
A responder object is a class inheriting from the Responder class, so you should require the Responder class located in /lib
and create a child class.
When initialized, a responder will have accessor methods for the name of the bot (bot_name
) and for the parameters of the responder coming from the config file (params
).
For our example we add a clock_responder.rb file to the new app/responders/myorganization dir.
It declares the responder class in the myorganization module.
require_relative '../../lib/responder'
module Myorganization
class ClockResponder < Responder
end
end
Keyname
Using keyname
you can define the handle for the responder to be used in the configuration file. Using a symbol is ok.
For our example we’ll just use clock:
require_relative '../../lib/responder'
module Myorganization
class ClockResponder < Responder
keyname :clock
end
end
Now we can use the responder adding it to the config.yml file:
...
responders:
clock:
...
Define listening
The define_listening
method is the place to specify what the responder is listening to.
You can set values for two instance variables here:
@event_action: the action that triggered the event the responder will listen to
@event_regex: (optional) a regular expression the text body of the event (a comment or the body of an issue) should match for the responder to respond
When an event is sent from the reviews repository to Buffy, only responders that match action and regex (if present) will be run.
Event action
If you are listening to creation of issues, @event_action should be
"issues.opened"
.If you are listening to new comments, @event_action should be
"issue_comment.created""
.
Event regex
The @event_regex variable is where the syntax of every specific command is declared. If it is nil
the responder will respond to every event that matches @event_action.
Inside this method you have available the name of the bot in the @botname
instace variable and all the parameters for this responder from the config file in the @params
instance variable.
For our example, we will be listening to comments and we want the command to be “what time is it?”:
require_relative '../../lib/responder'
module Myorganization
class ClockResponder < Responder
keyname :clock
def define_listening
@event_action = "issue_comment.created"
@event_regex = /\A@#{bot_name} what time is it\?\s*\z/i
end
end
end
Mandatory parameters
You can also declare inside this method which parameters are required in the configuration using required_params
. This will create a reader method for every required parameter.
For example, we could make the command for invoking our responder mandatory and declared in the config.yml file instead that in our regex, that way the command for our responder can be changed and be easily configured:
require_relative '../../lib/responder'
module Myorganization
class ClockResponder < Responder
keyname :clock
def define_listening
required_params :command
@event_action = "issue_comment.created"
@event_regex = /\A@#{bot_name} #{command}\s*\z/i
end
end
end
now the command must be added to the config file or the responder will error and not run:
...
responders:
clock:
command: tell me the time
...
But we don’t want to be too strict so, we’ll allow the command to be changed but by default we’ll have one. For that we’ll use an auxiliary instance method:
require_relative '../../lib/responder'
module Myorganization
class ClockResponder < Responder
keyname :clock
def define_listening
@event_action = "issue_comment.created"
@event_regex = /\A@#{bot_name} #{clock_command}\s*\z/i
end
def clock_command
params[:command] || "what time is it\\?"
end
end
end
Process message
The process_message
method will be called if an event reaches Buffy and it matches the action and the regex in the define_listening method.
It accepts a single argument: the message that triggered the call.
This method is the place of all the custom Ruby code needed to perform whatever is the responder does. To interact back with the reviews repository there are several methods available:
respond(message): will post a comment with the specified message string
respond_external_template(template_name, locals): will post a comment using a template and passing it the locals variables
update_body(mark, end_mark, text): will update the body of the issue between marks with the passed text
add_assignee(user): will add the passed user to the issue’s assignees
remove_assignee(user): will remove the passed user from the issue’s assignees
replace_assignee(old_user, new_user): will replace the passed old_user with new_user in the issue’s assignees
process_labeling: will add/remove labels as specified in the responder config params
If you need to access any matched data from the @event_regex you have them available via the match_data
array.
For our example we’ll just reply a comment with the time:
require_relative '../../lib/responder'
module Myorganization
class ClockResponder < Responder
...
def process_message(message)
respond(Time.now.strftime("⏱ The time is %H:%M:%S %Z, today is %d-%m-%Y ⏱"))
end
end
end
Description
Use the description
method to add a short description of what the responder does.
Our example responder replies with the current time:
require_relative '../../lib/responder'
module Myorganization
class ClockResponder < Responder
...
def description
"Get the current time"
end
end
end
Example invocation
To help users understand how to use the responder, use the example_invocation
to add an example of how the responder is triggered.
In our example responder we’ll use the command declared via config or the default one:
require_relative '../../lib/responder'
module Myorganization
class ClockResponder < Responder
...
def example_invocation
"@#{bot_name} #{params[:command] || 'what time is it?'}"
end
end
end
Sample custom responder
The final version of our clock responder (in app/responders/myorganization/clock_responder.rb
):
require_relative '../../lib/responder'
module Myorganization
class ClockResponder < Responder
keyname :clock
def define_listening
@event_action = "issue_comment.created"
@event_regex = /\A@#{bot_name} #{clock_command}\s*\z/i
end
def process_message(message)
respond(Time.now.strftime("⏱ The time is %H:%M:%S %Z, today is %d-%m-%Y ⏱"))
end
def clock_command
params[:command] || "what time is it\\?"
end
def description
"Get the current time"
end
def example_invocation
"@#{bot_name} #{params[:command] || 'what time is it?'}"
end
end
end
Adding its key to the configuration file in the responder settings:
buffy:
responders:
clock:
...
The responder should be available and ready to use:
Tests
Don’t forget to add tests for any new Responder you create. Buffy uses the RSpec test framework.
For our sample responder, we would create spec/responders/myorganization/clock_responder_spec.rb
require_relative "../../spec_helper.rb"
describe Myorganization::ClockResponder do
subject do
described_class
end
describe "listening" do
before { @responder = subject.new({env: {bot_github_user: "testbot"}}, {}) }
it "should listen to new comments" do
expect(@responder.event_action).to eq("issue_comment.created")
end
it "should define regex" do
expect(@responder.event_regex).to match("@testbot what time is it?")
expect(@responder.event_regex).to_not match("@testbot whatever")
end
it "should allow invocation with custom command" do
custom_responder = subject.new({env: {bot_github_user: "testbot"}},
{command: "tell me the time"})
expect(custom_responder.event_regex).to match("@testbot tell me the time")
expect(custom_responder.event_regex).to_not match("@botsci what time is it?")
end
end
describe "#process_message" do
before do
@responder = subject.new({env: {bot_github_user: "botsci"}}, {})
disable_github_calls_for(@responder)
end
it "should respond to github" do
timenow = Time.now
expected_response = timenow.strftime("⏱ The time is %H:%M:%S %Z, today is %d-%m-%Y ⏱")
expect(Time).to receive(:now).and_respond(timenow)
expect(@responder).to receive(:respond).with(expected_response)
@responder.process_message("@testbot what time is it?")
end
end
end
You can find more examples of responder specs in the /spec/responders
directory.