// 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.URL;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
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.KeyManager;
import javax.net.ssl.SSLSocketFactory;
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 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(settings);
}
catch (IOException 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(new KeyManager[] {}));
}
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;
}
try
{
if (!new URL(arguments[0]).getProtocol().matches("^https?$"))
{
invalidURL(channel, sender, arguments[0]);
return;
}
}
catch (MalformedURLException exception)
{
invalidURL(channel, sender, 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()
{
synchronized (remote)
{
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();
}
private void invalidURL(String channel, String sender, String url)
{
sendMessage(channel, sender, String.format("invalid URL (\"%1$s\")", url));
}
});
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;
synchronized (remote)
{
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()
{
synchronized (remote)
{
try
{
remote.startPairDevice(arguments[0]);
}
catch (GeneralSecurityException exception)
{
error(channel, sender, exception);
}
}
sendMessage(channel, sender, String.format("enter the code from the device (\"%1$s\") to finish pairing", arguments[0]));
}
}.start();
break;
default:
sendMessage(channel, sender, String.format("finishing pairing with device (\"%1$s\")...", arguments[0]));
new Thread()
{
@Override
public void run()
{
synchronized (remote)
{
remote.finishPairDevice(arguments[0], arguments[1]);
}
sendMessage(channel, sender, String.format("paired with device (\"%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);
}
}
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)
try
{
if (new URL(arguments[0]).getProtocol().matches("^https?$"))
{
commands.get("googletv").execute(channel, sender, admin, StringUtils.join(arguments, ' '));
return;
}
}
catch (MalformedURLException exception) {}
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");
}
private void help(String channel, String sender, boolean admin, String command)
{
commands.get("help").execute(channel, sender, admin, command);
}
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