package org.rhq.etc.ircbot;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.atlassian.jira.rest.client.domain.Issue;
import com.atlassian.util.concurrent.Effect;
import com.atlassian.util.concurrent.Promise;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.pircbotx.PircBotX;
import org.pircbotx.User;
import org.pircbotx.hooks.Listener;
import org.pircbotx.hooks.ListenerAdapter;
import org.pircbotx.hooks.events.DisconnectEvent;
import org.pircbotx.hooks.events.MessageEvent;
import org.pircbotx.hooks.events.NickChangeEvent;
import org.pircbotx.hooks.events.PrivateMessageEvent;
/**
* An IRC bot for doing helpful stuff on the Freenode #rhq channel.
*
* @author Ian Springer
* @author Jiri Kremser
*/
public class RhqIrcBotListener extends ListenerAdapter<RhqIrcBot> {
private static final Pattern BZ_PATTERN = Pattern.compile("(?i)(bz|bug)[ ]*(\\d{6,7})");
private static final String JIRA_PROJECT = "JON3-";
private static final Pattern JIRA_PATTERN = Pattern.compile("(?i)(" + JIRA_PROJECT + "\\d{1,5})");
private static final Pattern COMMIT_PATTERN = Pattern.compile("(?i)(\\!commit|cm)[ ]*([0-9a-f]{3,40})");
private static final Pattern ECHO_PATTERN = Pattern.compile("(?i)echo[ ]+(.+)");
private static final String COMMIT_LINK = "https://github.com/rhq-project/rhq/commit/%s";
private static final String PTO_LINK = "https://mail.corp.redhat.com/home/theute@redhat.com/Thomas%20Reports%20OOO?fmt=rss&view=day&start=0day&end=0day";
private static final DateFormat monthFormat = new SimpleDateFormat("MMM");
private static final DateFormat dayInMonthFormat = new SimpleDateFormat("d");
private static enum Command {
FORUM("Our forum is available from https://community.jboss.org/en/rhq?view=discussions", true), HELP(
"You can use one of the following commands: ", true), LISTS(
"Feel free to enroll to the user list https://lists.fedorahosted.org/mailman/listinfo/rhq-users"
+ " or the devel list https://lists.fedorahosted.org/mailman/listinfo/rhq-devel", true), LOGS(
"IRC logs are available from http://transcripts.jboss.org/channel/irc.freenode.org/%23rhq/index.html", true), PTO, SOURCE(
"The code could be viewed/cloned on https://github.com/rhq-project or https://git.fedorahosted.org/cgit/rhq/rhq.git/",
true), SUPPORT, WIKI("Our wiki is available from https://docs.jboss.org/author/display/RHQ/Home", true);
public static final String PREFIX = "!";
private final String staticRespond;
private final boolean includeInHelp;
Command(String staticRespond, boolean includeInHelp) {
this.staticRespond = staticRespond;
this.includeInHelp = includeInHelp;
}
Command() {
this(null, false);
}
}
private static final Set<String> JON_DEVS = new HashSet<String>();
static {
JON_DEVS.add("theute");
JON_DEVS.add("jkremser");
JON_DEVS.add("jsanda");
JON_DEVS.add("jshaughn");
JON_DEVS.add("lkrejci");
JON_DEVS.add("lzoubek");
JON_DEVS.add("mazz");
JON_DEVS.add("mtho11");
JON_DEVS.add("pilhuhn");
JON_DEVS.add("spinder");
JON_DEVS.add("stefan_n");
JON_DEVS.add("tsegismont");
JON_DEVS.add("lzoubek");
JON_DEVS.add("Yak");
}
private final String server;
private final String channel;
private final BugResolver bzResolver = new BugzillaResolver();
private final JiraResolver jiraResolver = new JiraResolver();
private final boolean isRedHatChannel;
private final Map<String, String> names = new HashMap<String, String>();
private final Map<String, String> ptoCache = new HashMap<String, String>();
private final Map<String, String> supportCache = new HashMap<String, String>();
private final Pattern commandPattern;
public RhqIrcBotListener(String server, String channel) {
this.server = server;
this.channel = channel;
isRedHatChannel = "irc.devel.redhat.com".equals(server) || "irc.lab.bos.redhat.com".equals(server);
if (isRedHatChannel)
System.out.print("Red Hat channel");
StringBuilder commandRegExp = new StringBuilder();
commandRegExp.append("^(?i)[ ]*").append(Command.PREFIX).append("(");
for (Command command : Command.values()) {
commandRegExp.append(command.name()).append('|');
}
commandRegExp.deleteCharAt(commandRegExp.length() - 1);
commandRegExp.append(')');
commandPattern = Pattern.compile(commandRegExp.toString());
}
@Override
public void onMessage(final MessageEvent<RhqIrcBot> event) throws Exception {
if (event.getUser().getNick().toLowerCase().contains("bot")) {
return; // never talk with artificial forms of life
}
final PircBotX bot = event.getBot();
if (!bot.getNick().equals(bot.getName())) {
bot.changeNick(bot.getName());
}
String message = event.getMessage();
// react to BZs in the messages
Matcher bzMatcher = BZ_PATTERN.matcher(message);
while (bzMatcher.find()) {
final String response = bzResolver.resolve(bzMatcher.group(2));
bot.sendMessage(event.getChannel(), response);
}
// react to Jira bugs in the messages
Matcher jiraMatcher = JIRA_PATTERN.matcher(message);
while (jiraMatcher.find()) {
// final String response = jiraResolver.resolve(bzMatcher.group(1));
// bot.sendMessage(event.getChannel(), response);
final String bugId = jiraMatcher.group(1);
final Promise<Issue> issuePromise = jiraResolver.resolveAsync(bugId);
issuePromise.done(new Effect<Issue>() {
@Override
public void apply(Issue a) {
bot.sendMessage(event.getChannel(), bugId + ": " + Color.RED + a.getSummary() + Color.NORMAL
+ ", priority: " + Color.GREEN + a.getPriority().getName() + Color.NORMAL + ", created: "
+ a.getCreationDate().toString("YYYY-MM-DD") + " [ " + JiraResolver.JIRA_URL + "/browse/"
+ bugId + " ]");
}
});
issuePromise.fail(new Effect<Throwable>() {
@Override
public void apply(Throwable e) {
bot.sendMessage(event.getChannel(),
"Failed to access bug " + bugId + " Cause: " + shorten(e.getMessage()));
}
});
}
// react to the commit hashs included in the messages
Matcher commitMatcher = COMMIT_PATTERN.matcher(message);
while (commitMatcher.find()) {
String shaHash = commitMatcher.group(2);
String response = String.format(COMMIT_LINK, shaHash);
bot.sendMessage(event.getChannel(), event.getUser().getNick() + ": " + response);
}
if (message.startsWith(event.getBot().getNick())) {
// someone asked bot directly, we have to remove that from message
message = message.substring(event.getBot().getNick().length());
message = message.replaceFirst("[^ ]*", "");
}
// react to commands included in the messages
Matcher commandMatcher = commandPattern.matcher(message);
while (commandMatcher.find()) {
Command command = Command.valueOf(commandMatcher.group(1).toUpperCase());
String response = prepareResponseForCommand(command);
if (response != null) {
bot.sendMessage(event.getChannel(), event.getUser().getNick() + ": " + shorten(response));
}
}
// ping JON devs
if (message.matches(".*\\b(jon-team|jboss-on-team)\\b.*")) {
Set<User> users = bot.getUsers(event.getChannel());
StringBuilder presentJonDevs = new StringBuilder();
for (User user : users) {
String nick = user.getNick();
if (JON_DEVS.contains(nick) && !nick.equals(event.getUser().getNick())) {
presentJonDevs.append(nick).append(' ');
}
}
bot.sendMessage(event.getChannel(), presentJonDevs + ": see message from " + event.getUser().getNick()
+ " above");
}
}
@Override
public void onPrivateMessage(PrivateMessageEvent<RhqIrcBot> privateMessageEvent) throws Exception {
PircBotX bot = privateMessageEvent.getBot();
String message = privateMessageEvent.getMessage();
Matcher echoMatcher = ECHO_PATTERN.matcher(message);
if (echoMatcher.matches()) {
if (!JON_DEVS.contains(privateMessageEvent.getUser().getNick())) {
privateMessageEvent.respond("You're not my master, I am your master, go away");
} else {
String echoMessage = echoMatcher.group(1);
bot.sendMessage(this.channel, echoMessage);
}
} else if (message.equalsIgnoreCase(Command.PREFIX + "listrenames")) {
//Generate a list of renames in the form of old1 changed to new1, old2 changed to new2, etc
StringBuilder users = new StringBuilder();
for (Map.Entry<String, String> curUser : names.entrySet()) {
users.append(curUser.getKey()).append(" changed to ").append(curUser.getValue()).append(", ");
}
String usersString = users.substring(0, users.length() - 3);
privateMessageEvent.respond("Renamed users: " + usersString);
} else {
boolean isCommand = false;
// react to commands included in the messages
Matcher commandMatcher = commandPattern.matcher(message);
while (commandMatcher.find()) {
isCommand = true;
Command command = Command.valueOf(commandMatcher.group(1).toUpperCase());
String response = prepareResponseForCommand(command);
if (response != null) {
bot.sendMessage(privateMessageEvent.getUser(), response);
}
}
if (!isCommand) {
bot.sendMessage(privateMessageEvent.getUser(), "Hi, I am " + bot.getFinger() + ".\n"
+ prepareResponseForCommand(Command.HELP));
}
}
}
@Override
public void onDisconnect(DisconnectEvent<RhqIrcBot> disconnectEvent) throws Exception {
boolean connected = false;
while (!connected) {
Thread.sleep(60 * 1000L); // 1 minute
try {
PircBotX newBot = new RhqIrcBot(this);
newBot.connect(this.server);
newBot.joinChannel(this.channel);
connected = true;
} catch (Exception e) {
System.err.println("Failed to reconnect to " + disconnectEvent.getBot().getServer() + " IRC server: "
+ e);
}
}
// Try to clean up the old bot, so it can release any memory, sockets, etc. it's using.
PircBotX oldBot = disconnectEvent.getBot();
Set<Listener> oldListeners = new HashSet<Listener>(oldBot.getListenerManager().getListeners());
for (Listener oldListener : oldListeners) {
oldBot.getListenerManager().removeListener(oldListener);
}
}
@Override
public void onNickChange(NickChangeEvent<RhqIrcBot> event) throws Exception {
//Store the result
names.put(event.getOldNick(), event.getNewNick());
}
private String prepareResponseForCommand(Command command) {
if (command.staticRespond != null) {
String response = command.staticRespond;
if (command == Command.HELP) {
for (Command com : Command.values()) {
if (com.includeInHelp) {
response += Command.PREFIX + com.toString().toLowerCase() + " ";
}
}
}
return response;
}
switch (command) {
case SUPPORT:
if (isRedHatChannel)
return whoIsOnSupport();
case PTO:
if (isRedHatChannel)
return whoIsOnPto(PTO_LINK);
default:
System.err.println("Unknown command:" + command);
break;
}
return null;
}
private String whoIsOnSupport() {
String month = monthFormat.format(new Date());
String dayInMonth = dayInMonthFormat.format(new Date());
String cachedValue = supportCache.get(month + "#" + dayInMonth);
if (cachedValue != null) {
return cachedValue;
}
String onSupport = GDocParser.onSupport1();
String value = doNotNotify(onSupport + " is on support this week");
supportCache.put(month + "#" + dayInMonth, value);
return value;
}
private String whoIsOnPto(String link) {
String month = monthFormat.format(new Date());
String dayInMonth = dayInMonthFormat.format(new Date());
String cachedValue = ptoCache.get(month + "#" + dayInMonth);
if (cachedValue != null) {
return cachedValue;
}
try {
String onPto = "";
Document doc = Jsoup.connect(link).ignoreContentType(true).get();
Elements titles = doc.select("rss channel item title");
for (Element title : titles) {
onPto += doNotNotify(title.text()) + ", ";
}
if (!onPto.isEmpty()) {
String value = doNotNotify(onPto.substring(0, onPto.length() - 2));
ptoCache.put(month + "#" + dayInMonth, value);
return value;
}
} catch (IOException e) {
e.printStackTrace();
}
return "no one is on PTO today";
}
private String doNotNotify(String nick) {
//replace all vowels with unicode chars that look same not to spam users with notifications
return nick.toLowerCase().replaceFirst("a", "\u0430").replaceFirst("e", "\u0435").replaceFirst("i", "\u0456")
.replaceFirst("o", "\u043E").replaceFirst("u", "\u222A").replaceFirst("y", "\u028F");
}
private String shorten(String message) {
if (message != null && message.length() > 300) {
return message.substring(0, 300) + "...";
} else
return message;
}
}