323 lines
9.5 KiB
JavaScript
323 lines
9.5 KiB
JavaScript
var irc = require('irc');
|
|
var express = require('express');
|
|
var bodyParser = require('body-parser');
|
|
var request = require('request');
|
|
var cheerio = require('cheerio');
|
|
|
|
var config = require('./config');
|
|
|
|
// Bind recipients to notifications.
|
|
var channels = [];
|
|
var hookToChannel = {};
|
|
for (var who in config.reports) {
|
|
if (who.indexOf('#') === 0)
|
|
channels.push(who);
|
|
|
|
var hooks = config.reports[who];
|
|
for (var i = 0; i < hooks.length; i++) {
|
|
var hook = hooks[i];
|
|
(hookToChannel[hook] = hookToChannel[hook] || []).push(who);
|
|
}
|
|
}
|
|
|
|
// Sanitize projectUrl
|
|
if (typeof config.projectUrl !== 'undefined') {
|
|
var url = '' + config.projectUrl;
|
|
if (url[url.length - 1] !== '/') {
|
|
url += '/';
|
|
}
|
|
config.projectUrl = url;
|
|
}
|
|
|
|
var client = new irc.Client(config.server, config.nick, {
|
|
debug: config.debug || false,
|
|
channels: channels,
|
|
userName: config.userName,
|
|
realName: config.realName,
|
|
retryDelay: 120000
|
|
});
|
|
|
|
var app = express();
|
|
|
|
app.use(bodyParser.json()); // for parsing application/json
|
|
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
|
|
|
|
var shortenURL = function(url, callback) { callback(url); };
|
|
|
|
if (config.lstu) {
|
|
if (config.lstu[config.lstu.length - 1] !== '/') {
|
|
config.lstu += '/';
|
|
}
|
|
|
|
shortenURL = function shortenURL(url, callback) {
|
|
request(config.lstu + 'a', { method: 'POST', form: { lsturl: url, format: 'json' } }, function (err, res, body) {
|
|
try {
|
|
body = JSON.parse(body);
|
|
} catch(err) {
|
|
body = {err: 'cant parse JSON'};
|
|
}
|
|
if (err || !body.success) {
|
|
console.error("Error when shortening link: ",
|
|
res ? "(status: " + res.statusCode + ")" : "(no response)",
|
|
'\nerror:', err,
|
|
'\nfailure reason:', body.msg);
|
|
callback(url);
|
|
} else {
|
|
callback(body.short);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
function conjugatePast(verb) {
|
|
// Make action displayable (e.g., open -> opened, close -> closed, merge -> merged).
|
|
return verb + (verb.substr(-1) === 'e' ? '' : 'e') + 'd';
|
|
}
|
|
|
|
var lastIssueActions = {};
|
|
|
|
var handlers = {
|
|
|
|
push: function(body, say) {
|
|
var user = body.user_username;
|
|
var projectName = body.project.name;
|
|
|
|
var commits = body.commits;
|
|
var numCommits = body.total_commits_count;
|
|
|
|
var branchName = body.ref.replace('refs/heads/', '');
|
|
var found = false;
|
|
for (var i = 0; i < config.branches.length; i++) {
|
|
if (branchName === config.branches[i]) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
return;
|
|
|
|
var msg = null;
|
|
if (!numCommits) {
|
|
// Special case: a branch was created or deleted.
|
|
var action = 'created';
|
|
if (body.after === '0000000000000000000000000000000000000000')
|
|
action = 'deleted';
|
|
msg = user + ' ' + action + ' branch ' + branchName + ' on ' + projectName + '.';
|
|
say(msg);
|
|
} else {
|
|
var maybeS = numCommits === 1 ? '' : 's';
|
|
var lastCommit = commits[commits.length - 1];
|
|
var lastCommitMessage = lastCommit.message.trim().split('\n')[0].trim();
|
|
shortenURL(lastCommit.url, function(shortUrl) {
|
|
msg = user + ' pushed on ' + projectName + '@' + branchName + ': ';
|
|
if (numCommits === 1) {
|
|
msg += lastCommitMessage + ' ' + shortUrl;
|
|
} else {
|
|
msg += commits.length + ' commits (last: ' + lastCommitMessage + ') ' + shortUrl;
|
|
}
|
|
say(msg);
|
|
});
|
|
}
|
|
},
|
|
|
|
issue: function(body, say) {
|
|
var user = body.user.username;
|
|
var projectName = body.project.name;
|
|
|
|
var issue = body.object_attributes;
|
|
var issueNumber = issue.iid;
|
|
var issueTitle = issue.title.trim();
|
|
var issueState = issue.state;
|
|
var url = issue.url;
|
|
|
|
// Don't trigger the hook on issue's update;
|
|
if (issue.action === 'update')
|
|
return;
|
|
|
|
// Don't trigger several close event.
|
|
if (issue.action === lastIssueActions[issue.iid])
|
|
return;
|
|
lastIssueActions[issue.iid] = issue.action;
|
|
|
|
var displayedAction = conjugatePast(issue.action);
|
|
|
|
shortenURL(url, function(shortUrl) {
|
|
var msg = user + ' ' + displayedAction + ' issue #' + issueNumber + ' ("' + issueTitle + '") on ' + projectName + ' ' + shortUrl;
|
|
say(msg);
|
|
});
|
|
},
|
|
|
|
merge_request: function(body, say) {
|
|
var user = body.user.username;
|
|
|
|
var request = body.object_attributes;
|
|
|
|
var projectName = request.target.name;
|
|
|
|
var from = request.source_branch;
|
|
var to = request.target_branch;
|
|
|
|
var id = request.iid;
|
|
var title = request.title.trim();
|
|
var url = request.url;
|
|
var state = request.state;
|
|
|
|
// Don't trigger the hook on mr's updates.
|
|
if (request.action === 'update') {
|
|
return;
|
|
}
|
|
|
|
var displayedAction = conjugatePast(request.action);
|
|
|
|
shortenURL(url, function(shortUrl) {
|
|
var msg = user + ' ' + displayedAction + ' MR !' + id + ' (' + from + '->' + to + ': ' + title + ') ' +
|
|
' on ' + projectName + '; ' + shortUrl;
|
|
say(msg);
|
|
});
|
|
},
|
|
|
|
build: function(body, say) {
|
|
|
|
var id = body.build_id;
|
|
var status = body.build_status;
|
|
|
|
var isFinished = body.build_finished_at !== null;
|
|
var duration = body.build_duration;
|
|
|
|
var projectName = body.project_name;
|
|
var stage = body.build_stage;
|
|
|
|
var msg = projectName + ': build #' + id + ' (' + stage + ') changed status: ' + status;
|
|
if (isFinished)
|
|
msg += ' (finished in ' + duration + ' seconds.)';
|
|
|
|
say(msg);
|
|
}
|
|
|
|
};
|
|
|
|
function makeSay(body) {
|
|
var whom = hookToChannel[body.object_kind] || [];
|
|
return function say(msgs) {
|
|
if (!whom.length) {
|
|
return;
|
|
}
|
|
if (msgs) {
|
|
if (msgs instanceof Array) {
|
|
for (var i = 0; i < msgs.length; i++)
|
|
client.say(whom, msgs[i]);
|
|
} else {
|
|
client.say(whom, msgs);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
app.post('/', function(req, res) {
|
|
var body = req.body || {};
|
|
|
|
var msgs = null;
|
|
if (body.object_kind && handlers[body.object_kind])
|
|
handlers[body.object_kind](body, makeSay(body));
|
|
else
|
|
console.log("Unexpected object_kind:", body.object_kind);
|
|
|
|
res.sendStatus(200);
|
|
});
|
|
|
|
var chanMessageCounters = {};
|
|
|
|
var mentionCache = {};
|
|
|
|
function makeCacheKey(isIssue, id, chan) {
|
|
return chan + '-' + (isIssue ? 'issue' : 'mr') + id;
|
|
}
|
|
|
|
function fetch_and_say(isIssue, id, from, chan) {
|
|
var cacheKey = makeCacheKey(isIssue, id, chan);
|
|
|
|
// Don't mention if it's been already mentioned in the last
|
|
// config.cacheDuration messages.
|
|
var mentionCounter = mentionCache[cacheKey];
|
|
if (typeof mentionCounter !== 'undefined') {
|
|
if (chanMessageCounters[chan] - mentionCounter <= config.cacheDuration) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
var path, text_prefix;
|
|
if (isIssue) {
|
|
path = 'issues/';
|
|
text_prefix = 'Issue #';
|
|
} else {
|
|
path = 'merge_requests/';
|
|
text_prefix = 'Merge request !';
|
|
}
|
|
|
|
var to = chan === config.nick ? from : chan;
|
|
|
|
var url = config.projectUrl + path + id;
|
|
request(url, function(err, res, body) {
|
|
if (res && res.statusCode === 200) {
|
|
var title = cheerio.load(body)('head title').text();
|
|
if (title.length) {
|
|
client.say(to, title);
|
|
client.say(to, url);
|
|
} else {
|
|
client.say(to, text_prefix + id + ": " + url);
|
|
}
|
|
mentionCache[cacheKey] = chanMessageCounters[chan];
|
|
}
|
|
});
|
|
}
|
|
|
|
var issueRegexp = /(?:\s|^)#(\d+)/g;
|
|
var mergeRequestRegexp = /(?:\s|^)!(\d+)/g;
|
|
|
|
function testIssueRegexp(r) {
|
|
function test(input, expected) {
|
|
var match = r.exec(input);
|
|
var found = 0;
|
|
while (match !== null) {
|
|
if (match[1] !== expected[found].toString()) {
|
|
throw new Error('should have found ' + expected[found]);
|
|
}
|
|
found++;
|
|
match = r.exec(input);
|
|
}
|
|
if (expected.length !== found) {
|
|
throw new Error('missing expected occurrences: ' + expected.length + 'vs expected ' + found);
|
|
}
|
|
r.lastIndex = 0;
|
|
}
|
|
|
|
test('hello #301 jeej', [301]);
|
|
test('#302 lol', [302]);
|
|
test('lol#303', []);
|
|
test('lol #303', [303]);
|
|
test('\t#304', [304]);
|
|
test(' #305', [305]);
|
|
test('hello#305 #306 jeej#42 #307 #lol # #308', [306, 307, 308]);
|
|
};
|
|
|
|
testIssueRegexp(issueRegexp);
|
|
|
|
app.listen(config.port, config.hostname, function() {
|
|
console.log('gitlab-to-irc running.');
|
|
|
|
client.on('message', function(from, chan, message) {
|
|
chanMessageCounters[chan] = (chanMessageCounters[chan] || 0) + 1;
|
|
|
|
var matches = null;
|
|
while ((matches = issueRegexp.exec(message)) !== null) {
|
|
var issueId = matches[1];
|
|
fetch_and_say(true, issueId, from, chan);
|
|
}
|
|
|
|
while ((matches = mergeRequestRegexp.exec(message)) !== null) {
|
|
var mrId = matches[1];
|
|
fetch_and_say(false, mrId, from, chan);
|
|
}
|
|
});
|
|
});
|