Commit f84b7eef authored by Aorimn's avatar Aorimn

Add Irker service

Irker is a gateway which sends IRC messages on git updates. This new
service provides an interface to this gateway, integrated in Gitlab, for
each updates.
As per the guidelines, this commit adds the new feature in the
CHANGELOG, tests and documentation.

See http://www.catb.org/esr/irker/
parent 4e5bc1d5
......@@ -14,6 +14,7 @@ v 7.9.0 (unreleased)
- Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger)
- Fix mass-unassignment of issues (Robert Speicher)
- Allow user confirmation to be skipped for new users via API
- Add a service to send updates to an Irker gateway (Romain Coltel)
v 7.8.1
- Fix run of custom post receive hooks
......
......@@ -50,7 +50,8 @@ class Projects::ServicesController < Projects::ApplicationController
:room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch
:description, :issues_url, :new_issue_url, :restrict_to_branch,
:colorize_messages, :channels
)
end
end
......@@ -73,6 +73,7 @@ class Project < ActiveRecord::Base
has_one :gitlab_ci_service, dependent: :destroy
has_one :campfire_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy
has_one :flowdock_service, dependent: :destroy
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
# template :boolean default(FALSE)
require 'uri'
class IrkerService < Service
prop_accessor :colorize_messages, :recipients, :channels
validates :recipients, presence: true, if: :activated?
validate :check_recipients_count, if: :activated?
before_validation :get_channels
after_initialize :initialize_settings
# Writer for RSpec tests
attr_writer :settings
def initialize_settings
# See the documentation (doc/project_services/irker.md) for possible values
# here
@settings ||= {
server_ip: 'localhost',
server_port: 6659,
max_channels: 3,
default_irc_uri: nil
}
end
def title
'Irker (IRC gateway)'
end
def description
'Send IRC messages, on update, to a list of recipients through an Irker '\
'gateway.'
end
def help
msg = 'Recipients have to be specified with a full URI: '\
'irc[s]://irc.network.net[:port]/#channel. Special cases: if you want '\
'the channel to be a nickname instead, append ",isnick" to the channel '\
'name; if the channel is protected by a secret password, append '\
'"?key=secretpassword" to the URI.'
unless @settings[:default_irc].nil?
msg += ' Note that a default IRC URI is provided by this service\'s '\
"administrator: #{default_irc}. You can thus just give a channel name."
end
msg
end
def to_param
'irker'
end
def execute(push_data)
IrkerWorker.perform_async(project_id, channels,
colorize_messages, push_data, @settings)
end
def fields
[
{ type: 'textarea', name: 'recipients',
placeholder: 'Recipients/channels separated by whitespaces' },
{ type: 'checkbox', name: 'colorize_messages' },
]
end
private
def check_recipients_count
return true if recipients.nil? || recipients.empty?
if recipients.split(/\s+/).count > max_chans
errors.add(:recipients, "are limited to #{max_chans}")
end
end
def max_chans
@settings[:max_channels]
end
def get_channels
return true unless :activated?
return true if recipients.nil? || recipients.empty?
map_recipients
errors.add(:recipients, 'are all invalid') if channels.empty?
true
end
def map_recipients
self.channels = recipients.split(/\s+/).map do |recipient|
format_channel default_irc_uri, recipient
end
channels.reject! &:nil?
end
def default_irc_uri
default_irc = @settings[:default_irc_uri]
if !(default_irc.nil? || default_irc[-1] == '/')
default_irc += '/'
end
default_irc
end
def format_channel(default_irc, recipient)
cnt = 0
url = nil
# Try to parse the chan as a full URI
begin
uri = URI.parse(recipient)
raise URI::InvalidURIError if uri.scheme.nil? && cnt == 0
rescue URI::InvalidURIError
unless default_irc.nil?
cnt += 1
recipient = "#{default_irc}#{recipient}"
retry if cnt == 1
end
else
url = consider_uri uri
end
url
end
def consider_uri(uri)
# Authorize both irc://domain.com/#chan and irc://domain.com/chan
if uri.is_a?(URI) && uri.scheme[/^ircs?$/] && !uri.path.nil?
# Do not authorize irc://domain.com/
if uri.fragment.nil? && uri.path.length > 1
uri.to_s
else
# Authorize irc://domain.com/smthg#chan
# The irker daemon will deal with it by concatenating smthg and
# chan, thus sending messages on #smthgchan
uri.to_s
end
end
end
end
......@@ -100,7 +100,8 @@ class Service < ActiveRecord::Base
def self.available_services_names
%w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla asana
emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira redmine custom_issue_tracker)
emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira
redmine custom_issue_tracker irker)
end
def self.create_from_template(project_id, template)
......
require 'json'
require 'socket'
class IrkerWorker
include Sidekiq::Worker
def perform(project_id, chans, colors, push_data, settings)
project = Project.find(project_id)
# Get config parameters
return false unless init_perform settings, chans, colors
repo_name = push_data['repository']['name']
committer = push_data['user_name']
branch = push_data['ref'].gsub(%r'refs/[^/]*/', '')
if @colors
repo_name = "\x0304#{repo_name}\x0f"
branch = "\x0305#{branch}\x0f"
end
# Firsts messages are for branch creation/deletion
send_branch_updates push_data, project, repo_name, committer, branch
# Next messages are for commits
send_commits push_data, project, repo_name, committer, branch
close_connection
true
end
private
def init_perform(set, chans, colors)
@colors = colors
@channels = chans
start_connection set['server_ip'], set['server_port']
end
def start_connection(irker_server, irker_port)
begin
@socket = TCPSocket.new irker_server, irker_port
rescue Errno::ECONNREFUSED => e
logger.fatal "Can't connect to Irker daemon: #{e}"
return false
end
true
end
def sendtoirker(privmsg)
to_send = { to: @channels, privmsg: privmsg }
@socket.puts JSON.dump(to_send)
end
def close_connection
@socket.close
end
def send_branch_updates(push_data, project, repo_name, committer, branch)
if push_data['before'] =~ /^000000/
send_new_branch project, repo_name, committer, branch
elsif push_data['after'] =~ /^000000/
send_del_branch repo_name, committer, branch
end
end
def send_new_branch(project, repo_name, committer, branch)
repo_path = project.path_with_namespace
newbranch = "#{Gitlab.config.gitlab.url}/#{repo_path}/branches"
newbranch = "\x0302\x1f#{newbranch}\x0f" if @colors
privmsg = "[#{repo_name}] #{committer} has created a new branch "
privmsg += "#{branch}: #{newbranch}"
sendtoirker privmsg
end
def send_del_branch(repo_name, committer, branch)
privmsg = "[#{repo_name}] #{committer} has deleted the branch #{branch}"
sendtoirker privmsg
end
def send_commits(push_data, project, repo_name, committer, branch)
return if push_data['total_commits_count'] == 0
# Next message is for number of commit pushed, if any
if push_data['before'] =~ /^000000/
# Tweak on push_data["before"] in order to have a nice compare URL
push_data['before'] = before_on_new_branch push_data, project
end
send_commits_count(push_data, project, repo_name, committer, branch)
# One message per commit, limited by 3 messages (same limit as the
# github irc hook)
commits = push_data['commits'].first(3)
commits.each do |hook_attrs|
send_one_commit project, hook_attrs, repo_name, branch
end
end
def before_on_new_branch(push_data, project)
commit = commit_from_id project, push_data['commits'][0]['id']
parents = commit.parents
# Return old value if there's no new one
return push_data['before'] if parents.empty?
# Or return the first parent-commit
parents[0].id
end
def send_commits_count(data, project, repo, committer, branch)
url = compare_url data, project.path_with_namespace
commits = colorize_commits data['total_commits_count']
new_commits = 'new commit'
new_commits += 's' if data['total_commits_count'] > 1
sendtoirker "[#{repo}] #{committer} pushed #{commits} #{new_commits} " \
"to #{branch}: #{url}"
end
def compare_url(data, repo_path)
sha1 = Commit::truncate_sha(data['before'])
sha2 = Commit::truncate_sha(data['after'])
compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare"
compare_url += "/#{sha1}...#{sha2}"
colorize_url compare_url
end
def send_one_commit(project, hook_attrs, repo_name, branch)
commit = commit_from_id project, hook_attrs['id']
sha = colorize_sha Commit::truncate_sha(hook_attrs['id'])
author = hook_attrs['author']['name']
files = colorize_nb_files(files_count commit)
title = commit.title
sendtoirker "#{repo_name}/#{branch} #{sha} #{author} (#{files}): #{title}"
end
def commit_from_id(project, id)
commit = Gitlab::Git::Commit.find(project.repository, id)
Commit.new(commit)
end
def files_count(commit)
files = "#{commit.diffs.count} file"
files += 's' if commit.diffs.count > 1
files
end
def colorize_sha(sha)
sha = "\x0314#{sha}\x0f" if @colors
sha
end
def colorize_nb_files(nb_files)
nb_files = "\x0312#{nb_files}\x0f" if @colors
nb_files
end
def colorize_url(url)
url = "\x0302\x1f#{url}\x0f" if @colors
url
end
def colorize_commits(commits)
commits = "\x02#{commits}\x0f" if @colors
commits
end
end
# Irker IRC Gateway
GitLab provides a way to push update messages to an Irker server. When
configured, pushes to a project will trigger the service to send data directly
to the Irker server.
See the project homepage for further info: http://www.catb.org/esr/irker/
## Needed setup
You will first need an Irker daemon. You can download the Irker code from its
gitorious repository on https://gitorious.org/irker: `git clone
git@gitorious.org:irker/irker.git`. Once you have downloaded the code, you can
run the python script named `irkerd`. This script is the gateway script, it acts
both as an IRC client, for sending messages to an IRC server obviously, and as a
TCP server, for receiving messages from the GitLab service.
If the Irker server runs on the same machine, you are done. If not, you will
need to follow the firsts steps of the next section.
## Optional setup
In the `app/models/project_services/irker_service.rb` file, you can modify some
options in the `initialize_settings` method:
- **server_ip** (defaults to `localhost`): the server IP address where the
`irkerd` daemon runs;
- **server_port** (defaults to `6659`): the server port of the `irkerd` daemon;
- **max_channels** (defaults to `3`): the maximum number of recipients the
client is authorized to join, per project;
- **default_irc_uri** (no default) : if this option is set, it has to be in the
format `irc[s]://domain.name` and will be prepend to each and every channel
provided by the user which is not a full URI.
If the Irker server and the GitLab application do not run on the same host, you
will **need** to setup at least the **server_ip** option.
## Note on Irker recipients
Irker accepts channel names of the form `chan` and `#chan`, both for the
`#chan` channel. If you want to send messages in query, you will need to add
`,isnick` avec the channel name, in this form: `Aorimn,isnick`. In this latter
case, `Aorimn` is treated as a nick and no more as a channel name.
Irker can also join password-protected channels. Users need to append
`?key=thesecretpassword` to the chan name.
......@@ -13,6 +13,7 @@ __Project integrations with external services for continuous integration and mor
- Gemnasium
- GitLab CI
- HipChat
- [Irker](irker.md) An IRC gateway to receive messages on repository updates.
- Pivotal Tracker
- Pushover
- Slack
......
......@@ -61,6 +61,12 @@ Feature: Project Services
And I fill email on push settings
Then I should see email on push service settings saved
Scenario: Activate Irker (IRC Gateway) service
When I visit project "Shop" services page
And I click Irker service link
And I fill Irker settings
Then I should see Irker service settings saved
Scenario: Activate Atlassian Bamboo CI service
When I visit project "Shop" services page
And I click Atlassian Bamboo CI service link
......
......@@ -17,6 +17,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
page.should have_content 'Atlassian Bamboo'
page.should have_content 'JetBrains TeamCity'
page.should have_content 'Asana'
page.should have_content 'Irker (IRC gateway)'
end
step 'I click gitlab-ci service link' do
......@@ -132,6 +133,22 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
find_field('Recipients').value.should == 'qa@company.name'
end
step 'I click Irker service link' do
click_link 'Irker (IRC gateway)'
end
step 'I fill Irker settings' do
check 'Active'
fill_in 'Recipients', with: 'irc://chat.freenode.net/#commits'
check 'Colorize messages'
click_button 'Save'
end
step 'I should see Irker service settings saved' do
find_field('Recipients').value.should == 'irc://chat.freenode.net/#commits'
find_field('Colorize messages').value.should == '1'
end
step 'I click Slack service link' do
click_link 'Slack'
end
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
require 'spec_helper'
require 'socket'
require 'json'
describe IrkerService do
describe 'Associations' do
it { should belong_to :project }
it { should have_one :service_hook }
end
describe 'Validations' do
before do
subject.active = true
subject.properties['recipients'] = _recipients
end
context 'active' do
let(:_recipients) { nil }
it { should validate_presence_of :recipients }
end
context 'too many recipients' do
let(:_recipients) { 'a b c d' }
it 'should add an error if there is too many recipients' do
subject.send :check_recipients_count
subject.errors.should_not be_blank
end
end
context '3 recipients' do
let(:_recipients) { 'a b c' }
it 'should not add an error if there is 3 recipients' do
subject.send :check_recipients_count
subject.errors.should be_blank
end
end
end
describe 'Execute' do
let(:irker) { IrkerService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
let(:recipients) { '#commits' }
let(:colorize_messages) { '1' }
before do
irker.stub(
active: true,
project: project,
project_id: project.id,
service_hook: true,
properties: {
'recipients' => recipients,
'colorize_messages' => colorize_messages
}
)
irker.settings = {
server_ip: 'localhost',
server_port: 6659,
max_channels: 3,
default_irc_uri: 'irc://chat.freenode.net/'
}
irker.valid?
@irker_server = TCPServer.new 'localhost', 6659
end
after do
@irker_server.close
end
it 'should send valid JSON messages to an Irker listener' do
irker.execute(sample_data)
conn = @irker_server.accept
conn.readlines.each do |line|
msg = JSON.load(line.chomp("\n"))
msg.keys.should match_array(['to', 'privmsg'])
if msg['to'].is_a?(String)
msg['to'].should == 'irc://chat.freenode.net/#commits'
else
msg['to'].should match_array(['irc://chat.freenode.net/#commits'])
end
end
conn.close
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment