com.darkenedsky.reddit.traders.RedditTraders.java Source code

Java tutorial

Introduction

Here is the source code for com.darkenedsky.reddit.traders.RedditTraders.java

Source

package com.darkenedsky.reddit.traders;

import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.ParseException;

import com.darkenedsky.reddit.traders.listener.About;
import com.darkenedsky.reddit.traders.listener.AcceptModInvite;
import com.darkenedsky.reddit.traders.listener.Activate;
import com.darkenedsky.reddit.traders.listener.Cake;
import com.darkenedsky.reddit.traders.listener.Confirm;
import com.darkenedsky.reddit.traders.listener.CountAllSubs;
import com.darkenedsky.reddit.traders.listener.GetList;
import com.darkenedsky.reddit.traders.listener.Help;
import com.darkenedsky.reddit.traders.listener.Install;
import com.darkenedsky.reddit.traders.listener.LastTrades;
import com.darkenedsky.reddit.traders.listener.Lookup;
import com.darkenedsky.reddit.traders.listener.ModHelp;
import com.darkenedsky.reddit.traders.listener.RedditListener;
import com.darkenedsky.reddit.traders.listener.Resolve;
import com.darkenedsky.reddit.traders.listener.SetAccountAgeRequirement;
import com.darkenedsky.reddit.traders.listener.SetBlameBan;
import com.darkenedsky.reddit.traders.listener.SetCheckBan;
import com.darkenedsky.reddit.traders.listener.SetDaysBetween;
import com.darkenedsky.reddit.traders.listener.SetFlair;
import com.darkenedsky.reddit.traders.listener.SetLegacy;
import com.darkenedsky.reddit.traders.listener.SetList;
import com.darkenedsky.reddit.traders.listener.SetModFlair;
import com.darkenedsky.reddit.traders.listener.SetTextFlair;
import com.darkenedsky.reddit.traders.listener.SetVerifiedEmail;
import com.darkenedsky.reddit.traders.listener.TopTraders;
import com.darkenedsky.reddit.traders.listener.Trade;
import com.darkenedsky.reddit.traders.listener.Undo;
import com.darkenedsky.reddit.traders.listener.ViewFlair;
import com.omrlnr.jreddit.messages.PrivateMessage;
import com.omrlnr.jreddit.subreddit.Subreddit;
import com.omrlnr.jreddit.utils.Utils;

/**
 * This bot responds to Reddit private messages in order to maintain an
 * Ebay-like feedback system for users of swap meet-style subreddits. It is
 * available under the MIT license.
 * 
 * Built for the fine folks at http://www.reddit.com/r/retrogameswap
 * 
 * Features include: - Allow users to record their own successful trades without
 * moderator involvement - Install, activate and deactivate in new subreddits
 * without interaction from the author - Look up a user's feedback or view a
 * leaderboard for the subreddit - Support legacy trades from before the bot was
 * responsible for a subreddit - Assign flair to users when they have reached
 * certain configurable threshholds - Provide a framework for dispute resolution
 * by a subreddit's moderators - Automatically ban users who are blamed for
 * enough unsuccessful trades in a window
 * 
 * @author Matt Holden (matt@mattholden.com)
 * 
 */
public class RedditTraders {

    public static RedditTraders instance;

    /**
     * The entry point for the RedditTraders application.
     * 
     * 
     * @param args
     *            Command line parameters (not used)
     */
    public static void main(String[] args) {
        instance = new RedditTraders();
        while (true) {
            instance.process();
            int sleep = instance.config.getSleepSec() * 1000;
            try {
                Thread.sleep(sleep);
            } catch (Exception x) {
                x.printStackTrace();
            }
        }
    }

    // only bother caching this locally, because checking in the DB would be too
    // damn slow.
    private ArrayList<String> messagesRead = new ArrayList<String>(10000);

    private HashMap<String, RedditListener> listeners = new HashMap<String, RedditListener>();

    /** Log4J instance */
    private final Logger LOG = Logger.getLogger(RedditTraders.class);

    /** The Configuration information we will load from config.xml */
    private Configuration config;

    /**
     * Construct a new RedditTraders instance.
     * 
     * 
     */
    public RedditTraders() {

        // Load XML configuration file, connect to DB and connect to Reddit API
        try {
            config = new Configuration();
            addListener(new Help(this));
            addListener(new About(this));
            addListener(new Confirm(this));
            addListener(new Activate(this));
            listeners.put("DEACTIVATE", new Activate(this));
            addListener(new CountAllSubs(this));
            addListener(new ModHelp(this));
            addListener(new AcceptModInvite(this));
            addListener(new Install(this));
            addListener(new Cake(this));
            addListener(new Trade(this));
            addListener(new TopTraders(this, "TOP20", 20));
            addListener(new Lookup(this));
            addListener(new SetLegacy(this));
            addListener(new SetTextFlair(this));
            addListener(new ViewFlair(this));
            addListener(new Resolve(this));
            listeners.put("BLAME", new Resolve(this));
            listeners.put("CLOSE", new Resolve(this));
            addListener(new SetFlair(this));
            listeners.put("REMOVEFLAIR", new SetFlair(this));
            addListener(new SetModFlair(this));
            listeners.put("REMOVEMODFLAIR", new SetModFlair(this));
            addListener(new SetBlameBan(this));
            addListener(new SetList(this));
            listeners.put("SETHAVELIST", new SetList(this));
            addListener(new GetList(this));
            listeners.put("HAVELIST", new GetList(this));
            addListener(new SetAccountAgeRequirement(this));
            addListener(new SetVerifiedEmail(this));
            addListener(new Undo(this));
            addListener(new LastTrades(this, "LAST10", 10));
            addListener(new SetCheckBan(this));
            addListener(new SetDaysBetween(this));

            // Build a system tray icon
            SystemTray tray = SystemTray.getSystemTray();

            PopupMenu popup = new PopupMenu();

            MenuItem todaysLog = new MenuItem("Today's Log");
            todaysLog.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    try {
                        new TextFrame("logs/RedditTraders.log").setVisible(true);
                    } catch (Exception e1) {
                        LOG.error(e1);
                    }
                }
            });
            popup.add(todaysLog);

            MenuItem exit = new MenuItem("Exit");
            ActionListener exitListener = new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    System.exit(0);
                }
            };
            exit.addActionListener(exitListener);
            popup.add(exit);

            Image image = Toolkit.getDefaultToolkit().getImage("reddit.png");
            TrayIcon trayIcon = new TrayIcon(image, "RedditTraders " + config.getVersion(), popup);
            trayIcon.setImageAutoSize(true);
            tray.add(trayIcon);

            LOG.debug("RedditTraders launched OK.");
        } catch (Exception x) {
            x.printStackTrace();
            System.exit(0);
        }
    }

    private void addListener(RedditListener listen) {
        listeners.put(listen.getCommand(), listen);
    }

    /**
     * Ban a user from the subreddit
     * 
     * @param user
     *            User to ban
     * @param subreddit
     *            subreddit to ban from
     * @param comment
     *            Reason for the ban
     * @throws ParseException
     * @throws IOException
     * @throws MalformedURLException
     */
    public void ban(String user, String subreddit, String comment)
            throws MalformedURLException, IOException, ParseException {
        String u = "http://www.reddit.com/api/friend";
        Utils.post("name=" + user + "&uh=" + config.getBotUser().getModhash() + "&type=banned&note=" + comment
                + "&r=" + subreddit, new URL(u), config.getBotUser().getCookie());

    }

    /**
     * Check to see if the bot is a moderator in the subreddit
     * 
     * 
     * @param subreddit
     *            Name of the subreddit
     * @return true if the bot is a moderator of it
     * 
     * @throws MalformedURLException
     * @throws IOException
     * @throws ParseException
     */
    public boolean botIsModerator(String subreddit) throws MalformedURLException, IOException, ParseException {

        Subreddit sub = new Subreddit();
        sub.setDisplayName(subreddit);
        List<String> mods = sub.getModerators(config.getBotUser());
        boolean isMod = mods.contains(config.getBotUser().getUsername());

        /**
         * //Don't send the message to the mods - it got the bot banned once \
         * if (!isMod) { sendMessage("/r/" + subreddit,
         * "RedditTraders Bot Install Not Complete", new StringBuffer(
         * "Hello. I am the RedditTraders trade resolution bot. One of the moderators of subreddit /r/"
         * + subreddit +
         * " asked me to monitor trades and user flair in your subreddit. However, one of the tasks I tried to do couldn't be completed because I don't have moderator access. "
         * +
         * "In order for all my functions to work properly, this account (/u/RedditTraders) needs to be a moderator of /r/"
         * + subreddit +
         * ". If you've decided you don't want me to monitor your trades anymore, send me a message that reads \"UNINSTALL "
         * + subreddit +
         * "\", which will command me to stop monitoring your subreddit. Thank you."
         * )); }
         */
        return isMod;
    }

    /**
     * Check if two users are banned, using the appropriate method defined in
     * the checkBan parameter
     * 
     * @param author
     *            first user to check
     * @param tradeWith
     *            second user to check
     * @param subreddit
     *            subreddit we are checking in (if not ALL)
     * @param checkBan
     *            checkban mode on the subreddit
     * @return true if either user is banned
     * @throws Exception
     */
    public boolean checkBans(String author, String tradeWith, String subreddit, int checkBan) throws Exception {

        // checking is turned off

        switch (checkBan) {
        case 1:
            return this.isBanned(author, tradeWith, subreddit);
        case 2:
            PreparedStatement ps = config.getJDBC().prepareStatement("select subreddit from subreddits;");
            ResultSet subs = ps.executeQuery();
            if (subs.first()) {
                while (true) {
                    if (isBanned(author, tradeWith, subs.getString("subreddit"))) {
                        subs.close();
                        return true;
                    }
                    if (subs.isLast()) {
                        break;
                    }
                    subs.next();
                }
            }
            subs.close();
            return false;

        // checking is turned off
        default:
            return false;
        }

    }

    /**
     * Evaluate the 'text' string to see if it is a valid command, and execute
     * it if it is.
     * 
     * 
     * @param pm
     *            The message the bot received
     * @param text
     *            The line of the message being evaluated
     * @param response
     *            A string buffer to write any output to, so that if a message
     *            contains multiple commands, they will all be responded to in
     *            the same reply message.
     * 
     * @return true if a command was found and executed
     * 
     * @throws MalformedURLException
     * @throws SQLException
     * @throws IOException
     * @throws ParseException
     */
    private boolean doCommand(PrivateMessage pm, String text, StringBuffer response)
            throws MalformedURLException, SQLException, IOException, ParseException {

        if (text == null || "".equals(text))
            return false;
        text = text.replaceAll("\t", " ");
        String[] tokens = text.split("[ ]");

        String command = tokens[0].toUpperCase();
        LOG.debug("Command: " + command);

        RedditListener listen = listeners.get(command);
        try {
            if (listen != null) {
                listen.doCommand(pm, tokens, response);
                return true;
            } else {
                return false;
            }
        } catch (Exception x) {
            LOG.error(x);
        }

        return false;
    }

    /**
     * Dump a JSONObject to the log.
     * 
     * @param object
     *            JSONObject
     */
    public void dump(Object object) {

        if (object instanceof JSONObject) {
            log("JSON Object");
            JSONObject job = (JSONObject) object;
            for (Object entry : job.entrySet()) {
                Map.Entry<Object, Object> map = (Map.Entry<Object, Object>) entry;
                log("Key: " + map.getKey() + " - Value: " + map.getValue());
            }
        } else if (object instanceof JSONArray) {
            log("JSON Array");
            JSONArray arr = (JSONArray) object;
            for (Object ob : arr) {
                JSONObject job = (JSONObject) ob;
                for (Object entry : job.entrySet()) {
                    Map.Entry<Object, Object> map = (Map.Entry<Object, Object>) entry;
                    log("Key: " + map.getKey() + " - Value: " + map.getValue());
                }
                log("------");
            }
        }
    }

    /**
     * Get the configuration settings.
     * 
     * @return the configuration.
     */
    public Configuration getConfig() {
        return config;
    }

    /**
     * Get the listener for the specified command.
     * 
     * @param cmd
     *            The command to look for
     * @return the listener attached to that command, or null if there is none.
     */
    public RedditListener getListener(String cmd) {
        return listeners.get(cmd.toUpperCase());
    }

    /**
     * Check if a user is banned
     * 
     * @param redditor
     *            redditor
     * @param redditor2
     *            another redditor to check in the same call (for efficiency)
     * @param subreddit
     *            subreddit name
     * @return true if the user is banned from the subreddit.
     */
    public boolean isBanned(String redditor, String redditor2, String subreddit) throws Exception {

        Subreddit sub = new Subreddit();
        sub.setDisplayName(subreddit);
        List<String> mods = sub.getBannedUsers(config.getBotUser());
        ArrayList<String> lcmods = new ArrayList<String>();
        for (String m : mods) {
            lcmods.add(m.toLowerCase());
        }
        return lcmods.contains(redditor.toLowerCase()) || lcmods.contains(redditor2.toLowerCase());
    }

    /**
     * A more general check to see if a user is the moderator of a subreddit
     * 
     * 
     * 
     * @param sender
     *            User we are testing
     * @param subreddit
     *            The subreddit we want the user to be a moderator of
     * @return true if the bot is a moderator of the subreddit
     * 
     * @throws MalformedURLException
     * @throws IOException
     * @throws ParseException
     */
    public boolean isModerator(String sender, String subreddit)
            throws MalformedURLException, IOException, ParseException {
        Subreddit sub = new Subreddit();
        sub.setDisplayName(subreddit);
        List<String> mods = sub.getModerators(config.getBotUser());

        return mods.contains(sender);
    }

    public void log(String string) {
        LOG.debug(string);
    }

    /**
     * Called in a loop every so many seconds to get messages and run any
     * commands found within them.
     * 
     */
    public void process() {
        List<PrivateMessage> messages = null;

        try {
            messages = config.getBotUser().getMessages("unread", 100);
            // LOG.debug("Found " + messages.size() + " new messages.");
        } catch (Exception x) {
            LOG.error(x);
            return;
        }

        for (PrivateMessage pm : messages) {

            // make sure we never process the same message twice, even if it
            // doesn't properly get marked read
            if (messagesRead.contains(pm.fullName)) {
                try {
                    pm.markRead(config.getBotUser(), true);

                } catch (Exception e) {
                    LOG.error(e);
                }
                continue;
            }
            messagesRead.add(pm.fullName);

            LOG.debug("======================================================");
            LOG.debug(
                    Calendar.getInstance().getTime() + " Received message from redditor " + pm.getAuthor() + ": ");
            LOG.debug("Subject: " + pm.getSubject());
            LOG.debug(pm.getBody());
            LOG.debug("\n");
            // Mark the message read
            try {
                pm.markRead(config.getBotUser(), true);

            } catch (Exception e) {
                LOG.error(e);
                continue;
            }

            boolean didSubject = false;
            int bodyCount = 0;
            StringBuffer response = new StringBuffer();

            try {
                didSubject = doCommand(pm, pm.getSubject(), response);
            } catch (Exception x) {
                LOG.error(x);
                response.append("An unknown error occurred while processing this command:\n\n " + pm.getSubject()
                        + "\n\n\n");
                bodyCount++;
            }
            String[] body = pm.getBody().split("[\n]");
            for (String s : body) {
                try {
                    if (doCommand(pm, s, response)) {
                        bodyCount++;
                    }
                } catch (Exception x) {
                    response.append("An unknown error occurred while processing this command:\n\n " + s + "\n\n\n");
                    bodyCount++;
                    LOG.error(x);
                }
            }

            if (!didSubject && bodyCount == 0 && pm.getAuthor().equals("RedditTraders") == false) {
                try {
                    getListener("HELP").process(pm, body, response);
                } catch (Exception x) {
                    response.append(
                            "An unknown error occurred while processing this command:\n\n " + body + "\n\n\n");
                    LOG.error(x);
                    x.printStackTrace();
                }
            }

            try {
                sendMessage(pm.getAuthor(), "RedditTraders Automated Message", response);
                LOG.debug("Sending to " + pm.getAuthor() + ":");
                LOG.debug(response.toString());
                LOG.debug("\n");

            } catch (Exception x) {
                LOG.error(x);
                x.printStackTrace();
            }

        }
    }

    /**
     * Render a percentage of two integers neatly
     * 
     * 
     * @param amt
     *            numerator
     * @param den
     *            denominator
     * @return the percentage as a whole percent
     */
    public String renderPct(int amt, int den) {
        double numerator = amt;
        double denominator = den;
        double pct = numerator / denominator;
        pct *= 100.0;
        int percent = (int) pct;
        return percent + "%";
    }

    /**
     * Check to see if the sender of this message is a moderator on a subreddit.
     * The subreddit's name must be the contents of tokens[1] to do the check.
     * If you want to check moderation on a message not formatted like this, use
     * the isModerator() method.
     * 
     * 
     * 
     * @param msg
     *            The private message received from the user.
     * @param tokens
     *            The individual "words" of the command we are executing
     * @return true if the bot is a moderator of the subreddit listed in
     *         tokens[1]
     * 
     * @throws MalformedURLException
     * @throws IOException
     * @throws ParseException
     * @see isModerator()
     */
    public boolean senderIsModerator(PrivateMessage msg, String[] tokens)
            throws MalformedURLException, IOException, ParseException {

        String sender = msg.getAuthor();
        LOG.debug("Sender: " + sender);

        if (tokens.length < 2) {
            return false;
        }

        String subreddit = tokens[1];
        return isModerator(sender, subreddit);
    }

    /**
     * Send a private message to a user on Reddit
     * 
     * 
     * @param user
     *            Username of the recipient
     * @param sub
     *            Subject of the message
     * @param body
     *            Body of the message
     * @throws MalformedURLException
     * @throws IOException
     * @throws ParseException
     */
    public void sendMessage(String user, String sub, StringBuffer body)
            throws MalformedURLException, IOException, ParseException {
        LOG.debug("Sending message " + sub + " to user " + user);
        new PrivateMessage(user, sub, body.toString()).send(config.getBotUser());

    }

    /**
     * Internal method to do the actual setting of flair
     * 
     * 
     * @param user
     *            Redditor's username
     * @param subreddit
     *            The subreddit being traded on
     * @param doTextFlair
     *            'true' if the TEXTFLAIR option is turned on for this subreddit
     * 
     * @throws MalformedURLException
     * @throws IOException
     * @throws ParseException
     * @throws SQLException
     */
    public void setUserFlair(String user, String subreddit, boolean doTextFlair)
            throws SQLException, MalformedURLException, IOException, ParseException {

        int trades = 0;
        String flair = null;

        PreparedStatement ps1 = config.getJDBC().prepareStatement(
                "select * from get_trade_count_with_countall((select redditorid from redditors where username ilike ?), (select redditid from subreddits where subreddit ilike ?));");
        ps1.setString(1, user);
        ps1.setString(2, subreddit);
        ResultSet set1 = ps1.executeQuery();
        if (set1.first()) {
            trades = set1.getInt("get_trade_count_with_countall");
        }
        set1.close();

        PreparedStatement ps2 = config.getJDBC().prepareStatement("select * from get_flair_class(?,?,?);");
        ps2.setString(1, user);
        ps2.setString(2, subreddit);
        ps2.setBoolean(3, isModerator(user, subreddit));
        ResultSet set2 = ps2.executeQuery();
        if (set2.first()) {
            flair = set2.getString("get_flair_class");
        }
        set2.close();
        if (flair == null) {
            LOG.debug("No flair matching criteria for user.");
            return;
        }

        String tradeCount = "";
        if (doTextFlair) {
            tradeCount = "&text=" + Integer.toString(trades) + " trade" + ((trades != 1) ? "s" : "");
        }

        LOG.debug("Posting flair...");
        String post = "uh=" + config.getBotUser().getModhash() + "&name=" + user + "&r=" + subreddit + "&css_class="
                + flair + tradeCount;
        LOG.debug(post);
        Utils.post(post, new URL("http://www.reddit.com/api/flair"), config.getBotUser().getCookie());
        LOG.debug("Flair posted.");

    }

}