// Big Screen Bot // // Douglas Thrift // // BigScreenBot.java /* Copyright 2011 Douglas Thrift * * This file is part of Big Screen Bot. * * Big Screen Bot is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Big Screen Bot is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Big Screen Bot. If not, see . */ package net.douglasthrift.bigscreenbot; import java.io.IOException; import java.net.MalformedURLException; import java.net.UnknownHostException; import java.net.URL; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.Set; import java.util.regex.Pattern; import javax.net.ssl.SSLSocketFactory; import com.google.polo.exception.PoloException; import com.google.polo.ssl.DummySSLSocketFactory; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.lang3.StringUtils; import org.jibble.pircbot.Colors; import org.jibble.pircbot.IrcException; public class BigScreenBot extends Bot { private static final int CHANNEL = 0x1; private static final int PRIVATE = 0x2; private static final int BOTH = 0x3; private static final Pattern SCHEMES = Pattern.compile("^https?$"); private static enum Action { RECONNECT, RESTART, QUIT } private abstract class Command { private boolean admin; private int access; private String arguments, description; protected Command(boolean admin, int access, String arguments, String description) { this.admin = admin; this.access = access; this.arguments = arguments; this.description = description; } public abstract void execute(String channel, String sender, boolean admin, String argument); public int getAccess() { return access; } public String getArguments() { return arguments; } public String getDescription() { return description; } public boolean isAdmin() { return admin; } public boolean isChannel() { return (access & CHANNEL) != 0; } public boolean isPrivate() { return (access & PRIVATE) != 0; } } private boolean verbose; private Remote remote; private Settings settings = new Settings(); private Set channels; private List admins = new ArrayList(); private Map bans = new TreeMap(); private Map commands = new TreeMap(); private Action action = Action.RECONNECT; private BigScreenBot(boolean verbose) { super(); this.verbose = verbose; try { settings.load(); remote = new Remote(verbose, settings); } catch (IOException exception) { error(exception, 1); } catch (GeneralSecurityException exception) { error(exception, 1); } setAutoNickChange(true); setFinger("Big Screen Bot"); setMessageDelay(0); setVersion(String.format("Big Screen Bot (%1$s)", System.getProperty("os.name"))); if (settings.getBooleanProperty("ssl", false)) if (settings.getBooleanProperty("verify", true)) setSocketFactory(SSLSocketFactory.getDefault()); else try { setSocketFactory(DummySSLSocketFactory.fromKeyManagers(null)); } catch (GeneralSecurityException exception) { error(exception, 1); } setLogin(System.getProperty("user.name")); setName(settings.getProperty("nick", "bigscreenbot")); setVerbose(verbose); channels = new HashSet(settings.getListProperty("channels", new ArrayList())); for (String admin : settings.getListProperty("admins")) admins.add(compileNickMask(admin)); for (String ban : settings.getListProperty("bans", new ArrayList())) bans.put(ban, compileNickMask(ban)); commands.put("ban", new Command(true, PRIVATE, "[mask...]", "block nick masks from using commands") { @Override public void execute(String channel, String sender, boolean admin, String argument) { String[] arguments = StringUtils.split(argument); if (arguments.length == 0) { listBans(channel, sender); return; } synchronized (bans) { for (String ban : arguments) if (bans.put(ban, compileNickMask(ban)) == null) sendMessage(channel, sender, String.format("banned nick mask (\"%1$s\")", ban)); else sendMessage(channel, sender, String.format("nick mask (\"%1$s\") already banned", ban)); storeBans(channel, sender); } } }); commands.put("googletv", new Command(false, BOTH, "url [device]", "fling url to a Google TV device") { @Override public void execute(final String channel, final String sender, boolean admin, String argument) { final String[] arguments = StringUtils.split(argument, null, 2); if (arguments.length == 0) { help(channel, sender, admin, "googletv"); return; } if (!isValidURL(arguments[0])) { sendMessage(channel, sender, String.format("invalid URL (\"%1$s\")", arguments[0])); return; } if (arguments.length == 2) sendMessage(channel, sender, String.format("flinging URL (\"%1$s\") to device (\"%2$s\")...", arguments[0], arguments[1])); else sendMessage(channel, sender, String.format("flinging URL (\"%1$s\") to device(s)...", arguments[0])); new Thread() { @Override public void run() { if (arguments.length == 2) remote.fling(arguments[1], arguments[0]); else remote.fling(arguments[0]); sendMessage(channel, sender, String.format("flung URL (\"%1$s\") to device(s)", arguments[0])); } }.start(); } }); commands.put("help", new Command(false, PRIVATE, "[command]", "show this help message") { @Override public void execute(String channel, String sender, boolean admin, String arguments) { String argument = null; Command command = null; try { argument = StringUtils.split(arguments, null, 2)[0].toLowerCase(); if (argument.startsWith("!")) argument = argument.substring(1); command = commands.get(argument); } catch (ArrayIndexOutOfBoundsException exception) {} sendMessage(channel, sender, Colors.BOLD + String.format("%1$-11s %2$-23s %3$-15s %4$s", "command", "arguments", "access", "description") + Colors.NORMAL); if (command != null) help(channel, sender, admin, argument, command); else for (Map.Entry nameCommand : commands.entrySet()) help(channel, sender, admin, nameCommand.getKey(), nameCommand.getValue()); } private void help(String channel, String sender, boolean admin, String name, Command command) { boolean unavailable = command.isAdmin() && !admin; String access; switch (command.getAccess()) { case CHANNEL: access = "channel"; break; case PRIVATE: access = "private"; break; case BOTH: default: access = "channel/private"; break; } sendMessage(channel, sender, (unavailable ? Colors.UNDERLINE : "") + String.format("%1$-11s %2$-23s %3$-15s %4$s", name, command.getArguments(), access, command.getDescription()) + (unavailable ? Colors.NORMAL : "")); } }); commands.put("join", new Command(true, PRIVATE, "channel", "join a channel") { @Override public void execute(String channel, String sender, boolean admin, String arguments) { String argument; try { argument = StringUtils.split(arguments, null, 2)[0]; } catch (ArrayIndexOutOfBoundsException exception) { help(channel, sender, admin, "join"); return; } joinChannel(argument); synchronized (channels) { channels.add(argument); storeChannels(channel, sender); } sendMessage(channel, sender, String.format("joined channel (\"%1$s\")", argument)); } }); commands.put("pair", new Command(true, PRIVATE, "[device [code]]", "pair with a Google TV device") { @Override public void execute(final String channel, final String sender, boolean admin, String argument) { final String[] arguments = StringUtils.split(argument, null, 2); switch (arguments.length) { case 0: sendMessage(channel, sender, "searching for devices to pair with..."); new Thread() { @Override public void run() { List devices; devices = remote.listDevices(); if (devices.isEmpty()) { sendMessage(channel, sender, "there are no devices to pair with"); return; } sendMessage(channel, sender, Colors.BOLD + "devices" + Colors.NORMAL); for (String device : devices) sendMessage(channel, sender, device); } }.start(); break; case 1: sendMessage(channel, sender, String.format("starting to pair with device (\"%1$s\")...", arguments[0])); new Thread() { @Override public void run() { try { if (remote.startPairDevice(arguments[0], new Runnable() { @Override public void run() { sendMessage(channel, sender, String.format("enter the code from the device (\"%1$s\") to finish pairing", arguments[0])); } })) sendMessage(channel, sender, String.format("pairing with device (\"%1$s\") succeeded", arguments[0])); else sendMessage(channel, sender, String.format("pairing with device (\"%1$s\") failed", arguments[0])); } catch (MalformedURLException exception) { sendMessage(channel, sender, String.format("invalid device name (\"%1$s\")", arguments[0])); } catch (UnknownHostException exception) { sendMessage(channel, sender, String.format("could not find device (\"%1$s\")", arguments[0])); } catch (IOException exception) { error(channel, sender, exception); } catch (PoloException exception) { error(channel, sender, exception); } catch (GeneralSecurityException exception) { error(channel, sender, exception); } } }.start(); break; default: sendMessage(channel, sender, String.format("finishing pairing with device (\"%1$s\")...", arguments[0])); new Thread() { @Override public void run() { try { if (!remote.finishPairDevice(arguments[0], arguments[1])) sendMessage(channel, sender, String.format("have not started pairing with device (\"%1$s\")", arguments[0])); } catch (MalformedURLException exception) { sendMessage(channel, sender, String.format("invalid device name (\"%1$s\")", arguments[0])); } } }.start(); } } }); commands.put("part", new Command(true, PRIVATE, "channel [message]", "part from a channel") { @Override public void execute(String channel, String sender, boolean admin, String argument) { String[] arguments = StringUtils.split(argument, null, 2); if (arguments.length == 0) { help(channel, sender, admin, "part"); return; } if (arguments.length == 2) partChannel(arguments[0], arguments[1]); else partChannel(arguments[0]); synchronized (channels) { channels.remove(arguments[0]); storeChannels(channel, sender); } sendMessage(channel, sender, String.format("parted channel (\"%1$s\")", arguments[0])); } }); commands.put("quit", new Command(true, PRIVATE, "[message]", "quit and do not come back") { @Override public void execute(String channel, String sender, boolean admin, String argument) { synchronized (action) { action = Action.QUIT; } quitServer(!argument.isEmpty() ? argument : "oh no!"); } }); commands.put("restart", new Command(true, PRIVATE, "", "quit and join running more up to date code") { @Override public void execute(String channel, String sender, boolean admin, String argument) { synchronized (action) { action = Action.RESTART; } quitServer("restarting"); } }); commands.put("say", new Command(true, PRIVATE, "nick|channel message", "say message to nick or channel") { @Override public void execute(String channel, String sender, boolean admin, String argument) { String[] arguments = StringUtils.split(argument, null, 2); if (arguments.length != 2) { help(channel, sender, admin, "say"); return; } if (arguments[0].equalsIgnoreCase(getNick())) { sendMessage(channel, sender, "nice try"); return; } sendMessage(arguments[0], arguments[1]); sendMessage(channel, sender, String.format("successfully sent message (\"%1$s\") to nick/channel (\"%2$s\")", arguments[1], arguments[0])); } }); commands.put("unban", new Command(true, PRIVATE, "[mask...]", "allow blocked nick masks to use commands again") { @Override public void execute(String channel, String sender, boolean admin, String argument) { String[] arguments = StringUtils.split(argument); if (arguments.length == 0) { listBans(channel, sender); return; } synchronized (bans) { for (String ban : arguments) if (bans.remove(ban) != null) sendMessage(channel, sender, String.format("unbanned nick mask (\"%1$s\")", ban)); else sendMessage(channel, sender, String.format("nick mask (\"%1$s\") already unbanned", ban)); storeBans(channel, sender); } } }); try { connect(settings.getProperty("server"), settings.getIntegerProperty("port", 6667)); } catch (IOException exception) { error(exception, 1); } catch (IrcException exception) { error(exception, 1); } } @Override public void dispose() { super.dispose(); try { remote.close(); } catch (IOException exception) { error(exception); } } public void sendMessage(String channel, String sender, String message) { sendMessage(channel != null ? channel : sender, message); } @Override protected void onConnect() { synchronized (channels) { for (String channel : channels) joinChannel(channel); } } @Override protected void onDisconnect() { synchronized (action) { switch (action) { case RESTART: dispose(); System.exit(2); case QUIT: dispose(); break; default: while (true) { try { reconnect(); } catch (IOException exception) { continue; } catch (IrcException exception) { error(exception, 1); } break; } } } } @Override protected void onMessage(String channel, String sender, String login, String hostname, String message) { doCommandFromMessage(channel, sender, login, hostname, message); } @Override protected void onPrivateMessage(String sender, String login, String hostname, String message) { doCommandFromMessage(null, sender, login, hostname, message); } private void doCommandFromMessage(String channel, String sender, String login, String hostname, String message) { boolean admin = matchNickMasks(sender, login, hostname, admins); synchronized (bans) { if (!admin && (matchNickMasks(sender, login, hostname, bans.values()) || !isNickInChannels(sender))) return; } message = Colors.removeFormattingAndColors(message); String[] arguments = StringUtils.split(message, null, 2); String argument = ""; Command command = null; if (arguments.length != 0) { if (channel == null) { if (isValidURL(arguments[0])) { fling(channel, sender, admin, arguments); return; } } else if (arguments.length == 2 && Pattern.compile("^" + Pattern.quote(getNick()) + ":?$", Pattern.CASE_INSENSITIVE).matcher(arguments[0]).matches() && isValidURL(StringUtils.split(arguments[1], null, 2)[0])) { fling(channel, sender, admin, Arrays.copyOfRange(arguments, 1, arguments.length)); return; } argument = arguments[0].toLowerCase(); if (argument.startsWith("!")) argument = argument.substring(1); command = commands.get(argument); } if (channel != null) { if (command == null) return; if (!admin && command.isAdmin()) return; if (!command.isChannel()) return; } else { if (command == null) { sendMessage(channel, sender, String.format("unknown command (\"%1$s\"); try \"help\"", argument)); return; } if (!admin && command.isAdmin()) { sendMessage(channel, sender, String.format("unauthorized command (\"%1$s\")", argument)); return; } if (!command.isPrivate()) { sendMessage(channel, sender, String.format("inappropriate command (\"%1$s\")", argument)); return; } } command.execute(channel, sender, admin, arguments.length == 2 ? arguments[1] : ""); } private void error(Exception exception) { if (verbose) exception.printStackTrace(); else System.err.println("bigscreenbot: " + exception.getMessage()); } private void error(Exception exception, int code) { error(exception); System.exit(code); } private void error(String channel, String sender, Exception exception) { error(exception); sendMessage(channel, sender, "an error occurred: " + exception.getMessage()); } private void fling(String channel, String sender, boolean admin, String... arguments) { commands.get("googletv").execute(channel, sender, admin, StringUtils.join(arguments, ' ')); } private void help(String channel, String sender, boolean admin, String command) { commands.get("help").execute(channel, sender, admin, command); } private boolean isValidURL(String url) { try { return SCHEMES.matcher(new URL(url).getProtocol()).matches(); } catch (MalformedURLException exception) { return false; } } private void listBans(String channel, String sender) { synchronized (bans) { if (bans.isEmpty()) { sendMessage(channel, sender, "there are no bans"); return; } sendMessage(channel, sender, Colors.BOLD + "ban" + Colors.NORMAL); for (String ban : bans.keySet()) sendMessage(channel, sender, ban); } } private void storeBans(String channel, String sender) { synchronized (bans) { synchronized (settings) { settings.setListProperty("bans", bans.keySet()); try { settings.store(); } catch (IOException exception) { error(channel, sender, exception); } } } } private void storeChannels(String channel, String sender) { synchronized (channels) { synchronized (settings) { settings.setListProperty("channels", channels); try { settings.store(); } catch (IOException exception) { error(channel, sender, exception); } } } } public static void main(String[] args) { Options options = new Options(); options.addOption("h", "help", false, "show this help message and exit"); options.addOption("v", "verbose", false, ""); CommandLine line = null; try { line = new GnuParser().parse(options, args); } catch (ParseException exception) { System.err.println("bigscreenbot: " + exception.getMessage()); } if (line.hasOption('h')) { new HelpFormatter().printHelp("bigscreenbot", options, true); return; } new BigScreenBot(line.hasOption('v')); } } // vim: expandtab