net.jimj.automaton.Bot.java Source code

Java tutorial

Introduction

Here is the source code for net.jimj.automaton.Bot.java

Source

/*
 * Copyright (c) <2013> <Jim Johnson jimj@jimj.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package net.jimj.automaton;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.mongodb.DB;
import com.mongodb.MongoException;
import net.jimj.automaton.commands.Command;
import net.jimj.automaton.commands.HeadCommand;
import net.jimj.automaton.commands.HelpCommand;
import net.jimj.automaton.commands.KarmaCommand;
import net.jimj.automaton.commands.NoteCommand;
import net.jimj.automaton.commands.PrefCommand;
import net.jimj.automaton.commands.Processor;
import net.jimj.automaton.commands.QuoteCommand;
import net.jimj.automaton.commands.UserCommand;
import net.jimj.automaton.commands.WeatherCommand;
import net.jimj.automaton.commands.YourMomCommand;
import net.jimj.automaton.commands.ZingCommand;
import net.jimj.automaton.events.BehaviorEvent;
import net.jimj.automaton.events.DataStoredEvent;
import net.jimj.automaton.events.HelpEvent;
import net.jimj.automaton.events.ReplyEvent;
import net.jimj.automaton.events.UnknownUserEvent;
import net.jimj.automaton.irc.CommandResponse;
import net.jimj.automaton.irc.IRCClient;
import net.jimj.automaton.irc.IRCConnection;
import net.jimj.automaton.irc.Message;
import net.jimj.automaton.model.Config;
import net.jimj.automaton.model.Stats;
import net.jimj.automaton.model.User;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLContexts;
import org.mongojack.JacksonDBCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

public class Bot {
    private static final Logger LOGGER = LoggerFactory.getLogger(Bot.class);
    private static final int END_OF_MOTD = 376;

    private ObjectMapper objectMapper;
    private HashMap<String, Command> commandMap = new HashMap<>();
    private ArrayList<Processor> processors = new ArrayList<>();
    private IRCClient ircClient;

    private UserAccess userAccess;
    private Config config;
    private DB db;
    private StatsCollector statsCollector;
    private EventBus eventBus;

    public Bot(DB db) {
        this.eventBus = new EventBus();
        eventBus.register(this);
        this.db = db;
        this.statsCollector = new StatsCollector();
        this.objectMapper = new ObjectMapper();
        this.userAccess = new UserAccess(db.getCollection("users"));

        loadConfig();
        loadCommands();
    }

    public static InputStream getConfigAsStream(String propName, String defaultLocation)
            throws FileNotFoundException {
        InputStream inputStream;
        String location = System.getProperty(propName);

        if (location != null) {
            inputStream = new FileInputStream(location);
        } else {
            inputStream = Bot.class.getResourceAsStream(defaultLocation);
        }

        return inputStream;
    }

    public void go() {
        LOGGER.debug("Creating statsCollector thread");
        Runnable collector = new Runnable() {
            @Override
            public void run() {
                JacksonDBCollection<Stats, String> statsCollection = JacksonDBCollection
                        .wrap(db.getCollection("botstats"), Stats.class, String.class);
                Stats lastStats = null;
                while (true) {
                    try {
                        Stats currentStats = statsCollector.toStats();
                        //If the current stats are different or the last stats are over an hour old
                        if (!currentStats.equals(lastStats)
                                || (currentStats.getRuntimeMillis() - lastStats.getRuntimeMillis()) > TimeUnit.HOURS
                                        .toMillis(1)) {
                            statsCollection.insert(currentStats);
                            lastStats = currentStats;
                        }
                        Thread.sleep(TimeUnit.SECONDS.toMillis(15));
                    } catch (InterruptedException e) {
                        //Shutting down.
                        statsCollection.insert(statsCollector.toStats());
                        break;
                    } catch (MongoException me) {
                        LOGGER.error("Error in stats collection", me);
                    }
                }
            }
        };
        new Thread(collector).start();
        connect();
    }

    @Subscribe
    public void execute(ReplyEvent event) {
        ircClient.sendPrivMsg(event.getTarget(), event.getMessage());
    }

    @Subscribe
    public void execute(HelpEvent event) {
        String cmdName = event.getCommandName();
        Command cmd = commandMap.get(cmdName);
        if (cmdName == null) {
            StringBuilder commandList = new StringBuilder();
            for (String command : commandMap.keySet()) {
                commandList.append(command).append(", ");
            }
            commandList.replace(commandList.length() - 2, commandList.length(), "");
            ircClient.sendPrivMsg(event.getTarget(), "Command list: " + commandList.toString());
        } else if (cmd == null) {
            ircClient.sendPrivMsg(event.getTarget(), "Unknown command: " + cmdName);
        } else {
            cmd.help(event.getUser());
        }
    }

    @Subscribe
    public void execute(UnknownUserEvent event) {
        if (event.isSelf()) {
            ircClient.sendPrivMsg(event.getTarget(),
                    "Sorry " + event.getUser().getNick() + " but I don't know who you are.");
        } else {
            ircClient.sendPrivMsg(event.getTarget(), "Sorry " + event.getUser().getNick() + " but I don't know who "
                    + event.getUnknownNick() + " is.");
        }
    }

    @Subscribe
    public void execute(DataStoredEvent event) {
        ircClient.sendPrivMsg(event.getTarget(), event.getMessage());
    }

    @Subscribe
    public void execute(BehaviorEvent event) {
        ircClient.sendPrivMsg(event.getTarget(), event.getResponse());
    }

    private void connect() {
        LOGGER.debug("Connecting...");
        try {
            SSLContext ctx = null;
            if (StringUtils.isNotBlank(config.getIrcKeystoreLocation())) {
                SSLContextBuilder ctxBuilder = SSLContexts.custom();
                KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
                try (FileInputStream keyInStream = new FileInputStream(
                        new File(config.getIrcKeystoreLocation()));) {
                    trustStore.load(keyInStream, config.getIrcKeystorePassword().toCharArray());
                }
                ctxBuilder.loadTrustMaterial(trustStore);
                ctx = ctxBuilder.build();
            }
            IRCConnection.Builder connectionBuilder = IRCConnection.builder().withHost(config.getServer())
                    .withNick(config.getNick());
            if (config.getPort() != -1) {
                connectionBuilder = connectionBuilder.withPort(config.getPort()).withSSLContext(ctx);
            }
            IRCConnection ircConnection = connectionBuilder.withListener(this).build();
            ircConnection.connect();
            ircClient = new IRCClient(ircConnection);
        } catch (Exception e) {
            LOGGER.error("Exception in go method ", e);
        }
    }

    @Subscribe
    public void onCommandResponse(CommandResponse response) {
        if (response.getCode() == END_OF_MOTD) {
            //End of MOTD.
            for (String channel : config.getChannels()) {
                ircClient.joinChannel(channel);
            }
        }
    }

    @Subscribe
    public void onMessage(Message message) {
        if (message.getType().equals("PRIVMSG")) {
            String from = message.getFrom();
            //Extract irc username
            if (from.contains("!")) {
                from = from.substring(0, from.indexOf("!"));
            }
            if (from.startsWith(":")) {
                from = from.substring(1);
            }

            User user = userAccess.findUser(from);

            String channel = null;
            String messageText = message.getMessage();
            //Extract the channel, if present.  Also chop off the leading : from the message.
            if (!messageText.startsWith(":")) {
                channel = messageText.substring(0, messageText.indexOf(":")).trim();
                messageText = messageText.substring(messageText.indexOf(":") + 1);
            } else {
                messageText = messageText.substring(1);
            }

            user.setChannel(channel);
            processMessage(user, messageText);
        } else {
            LOGGER.warn(
                    "Not sure what to do with message type " + message.getType() + " from " + message.getFrom());
        }
    }

    protected void processMessage(User user, String message) {
        //Ignore self for any processing.
        if (user.getNick().equals(config.getNick())) {
            return;
        }

        statsCollector.incMessageCount();
        if (message.startsWith(config.getCommandChar())) {
            statsCollector.incCommandCount();
            String commandName = message;

            //Try to chop off the first word
            int commandNameEnd = message.indexOf(" ");
            String args = null;

            //Strip off the command character for looking up the command
            if (commandNameEnd != -1) {
                commandName = message.substring(config.getCommandChar().length(), commandNameEnd);
                args = StringUtils
                        .strip(message.substring(config.getCommandChar().length() + commandName.length()));
            } else {
                commandName = commandName.substring(config.getCommandChar().length());
            }

            try {
                fireCommand(user, commandName, args);
            } catch (Exception e) {
                eventBus.post(new BehaviorEvent(user, BehaviorEvent.Behavior.SASSY));
                LOGGER.error("Caused by " + user.getNick() + " with message " + message, e);
            }
        } else {
            for (Processor processor : processors) {
                if (processor.shouldProcess(message)) {
                    statsCollector.incProcessCount();
                    processor.process(user, message);
                }
            }
        }
    }

    protected void fireCommand(User user, String commandName, String args) {
        LOGGER.debug("Firing command " + commandName);
        Command commandObj = commandMap.get(commandName);
        if (commandObj != null) {
            commandObj.execute(user, args);
        } else {
            eventBus.post(new BehaviorEvent(user, BehaviorEvent.Behavior.CONFUSED, BehaviorEvent.Behavior.SASSY));
        }
    }

    private void loadCommands() {
        loadCommand(new QuoteCommand(db.getCollection("quotes")));
        loadCommand(new HeadCommand());
        loadCommand(new KarmaCommand(db.getCollection("karma")));
        loadCommand(new YourMomCommand(db.getCollection("yourmom")));
        loadCommand(new NoteCommand(db.getCollection("notes")));
        loadCommand(new HelpCommand());
        loadCommand(new UserCommand(userAccess));
        loadCommand(new PrefCommand(userAccess));
        loadCommand(new WeatherCommand());
        loadCommand(new ZingCommand());
    }

    private void loadCommand(Command command) {
        command.setEventBus(eventBus);
        commandMap.put(command.getCommandName(), command);

        if (command instanceof Processor) {
            processors.add((Processor) command);
        }
    }

    private void loadConfig() {
        try {
            InputStream configStream = getConfigAsStream("bot.config", "/META-INF/config.json");
            config = objectMapper.readValue(configStream, Config.class);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(objectMapper.writeValueAsString(config));
            }
        } catch (IOException e) {
            System.err.println("Error loading configuration");
            e.printStackTrace(System.err);
            throw new IllegalStateException("Error loading configuration");
        }
    }

    private void messageOwner(String message) {
        String owner = userAccess.getOwnerNick();
        if (owner != null) {
            ircClient.sendPrivMsg(owner, message);
        }
    }
}