net.douglasthrift.bigscreenbot.BigScreenBot.java Source code

Java tutorial

Introduction

Here is the source code for net.douglasthrift.bigscreenbot.BigScreenBot.java

Source

// 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 <http://www.gnu.org/licenses/>.
 */

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<String> channels;
    private List<Pattern> admins = new ArrayList<Pattern>();
    private Map<String, Pattern> bans = new TreeMap<String, Pattern>();
    private Map<String, Command> commands = new TreeMap<String, Command>();
    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<String>(settings.getListProperty("channels", new ArrayList<String>()));

        for (String admin : settings.getListProperty("admins"))
            admins.add(compileNickMask(admin));

        for (String ban : settings.getListProperty("bans", new ArrayList<String>()))
            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<String, Command> 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<String> 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 Remote.Pairing() {
                                    @Override
                                    public void prompt() {
                                        sendMessage(channel, sender, String.format(
                                                "enter the code from the device (\"%1$s\") to finish pairing",
                                                arguments[0]));
                                    }

                                    @Override
                                    public void interrupted(InterruptedException exception) {
                                        sendMessage(channel, sender, String.format(
                                                "pairing with device (\"%1$s\") interrupted", arguments[0]));
                                    }
                                }))
                                    sendMessage(channel, sender,
                                            String.format("paired with device (\"%1$s\")", 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);
                            }
                        }
                    }.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