com.omertron.slackbot.listeners.BoardGameListener.java Source code

Java tutorial

Introduction

Here is the source code for com.omertron.slackbot.listeners.BoardGameListener.java

Source

/*
 *      Copyright (c) 2017 Stuart Boston
 *
 *      This file is part of the BGG Slack Bot.
 *
 *      The BGG Slack 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
 *      any later version.
 *
 *      The BGG Slack 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 the BGG Slack Bot.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.omertron.slackbot.listeners;

import com.omertron.bgg.BggException;
import com.omertron.bgg.enums.HotItemType;
import com.omertron.bgg.enums.IncludeExclude;
import com.omertron.bgg.model.BoardGameExtended;
import com.omertron.bgg.model.CollectionItem;
import com.omertron.bgg.model.CollectionItemWrapper;
import com.omertron.bgg.model.HotListItem;
import com.omertron.bgg.model.IdValue;
import com.omertron.bgg.model.OwnerStatus;
import com.omertron.bgg.model.RankedList;
import com.omertron.bgg.model.SearchWrapper;
import com.omertron.bgg.model.Thing;
import com.omertron.bgg.model.UserInfo;
import com.omertron.slackbot.Constants;
import static com.omertron.slackbot.Constants.DELIM_LEFT;
import static com.omertron.slackbot.Constants.DELIM_RIGHT;
import com.omertron.slackbot.enumeration.ExitCode;
import com.omertron.slackbot.enumeration.StatCategory;
import com.omertron.slackbot.functions.BotStatistics;
import com.omertron.slackbot.functions.BotWelcome;
import com.omertron.slackbot.functions.Meetup;
import com.ullink.slack.simpleslackapi.*;
import com.ullink.slack.simpleslackapi.events.SlackMessagePosted;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yamj.api.common.exception.ApiException;

/**
 *
 * @author Omertron
 */
public class BoardGameListener extends AbstractListener {

    private static final Logger LOG = LoggerFactory.getLogger(BoardGameListener.class);
    private static final Pattern PAT_COMMAND;
    private static final Pattern PAT_ADMIN;
    private static final Pattern PAT_COLL_PARAM = Pattern.compile("^(\\w*)(\\s(.+))?$");
    private static final String BGG_ID = "BGG ID";
    private static final String USERNAME = "username";
    private static final String INFORMATION_ON = "Information on ";
    private static final String UNKNOWN = " (Unknown)";

    static {
        List<String> commands = new ArrayList<>();
        commands.add("search");
        HelpListener.addHelpMessage(10, "search", "game name",
                "Search for games called *<game name>*.\nThis does not need to be the exact name of the game.",
                false);
        commands.add("game");
        HelpListener.addHelpMessage(11, "game", BGG_ID, "Get information about the game with this *<BGG ID>*.",
                false);
        HelpListener.addHelpMessage(12, "game", "game title",
                "Get information about *<game title>*.\nThis must be the exact name of the game as per BGG.",
                false);
        commands.add("coll");
        HelpListener.addHelpMessage(15, "coll", USERNAME, "Get a list of the owned games for a BGG user.", false);
        HelpListener.addHelpMessage(16, "coll", new String[] { USERNAME, "ID list" },
                "Get a list of the owned games for a BGG user that match the ID list.", false);
        commands.add("user");
        HelpListener.addHelpMessage(20, "user", USERNAME, "Get information on a BGG user.", false);
        commands.add("meetup");
        HelpListener.addHelpMessage(25, "meetup", new String[] { "Quantity", "DETAILED" },
                "Get a list of the *<Quantity>* upcoming MeetUps.\nAdd the *<Detailed>* keyword to get more information.",
                false);
        commands.add("hot");
        HelpListener.addHelpMessage(26, "hot", new String[] { "boardgame", "person", "company" },
                "Get the top 10 items from the category passed.\nDefault, if empty, is boardgames.", false);

        String regex = new StringBuilder("(?i)").append("\\").append(DELIM_LEFT).append("\\").append(DELIM_LEFT)
                .append("(").append(StringUtils.join(commands, "|")).append(")(?:\\W*)(.*)").append("\\")
                .append(DELIM_RIGHT).append("\\").append(DELIM_RIGHT).toString();

        LOG.info("Command Pattern: {}", regex);
        PAT_COMMAND = Pattern.compile(regex);

        commands.clear();
        commands.add("welcome");
        HelpListener.addHelpMessage(97, "welcome", "user", "Send welcome message to *<user>*", true);
        commands.add("restart");
        HelpListener.addHelpMessage(98, "restart", "",
                "Shutdown and restart the bot.\nThis is used to upgrade the bot to the latest version", true);
        commands.add("quit");
        HelpListener.addHelpMessage(99, "quit", "",
                "Shutdown the bot.\nNote the bot will need to be manually restarted", true);

        // (?:<.+?>\W+|\[\[){1}?(quit|restart)(.*?)(?:\]\])?$
        regex = new StringBuilder("(?:<.+?>\\W+|").append("\\").append(DELIM_LEFT).append("\\").append(DELIM_LEFT)
                .append("){1}?(").append(StringUtils.join(commands, "|")).append(")(.*?)(?:").append("\\")
                .append(DELIM_RIGHT).append("\\").append(DELIM_RIGHT).append(")?$").toString();

        LOG.info("Admin Pattern: {}", regex);
        PAT_ADMIN = Pattern.compile(regex);
    }

    @Override
    public void onEvent(SlackMessagePosted event, SlackSession session) {
        // Channel On Which Message Was Posted
        SlackChannel msgChannel = event.getChannel();
        String msgContent = event.getMessageContent();
        SlackUser msgSender = event.getSender();

        // Filter out the bot's own messages
        if (session.sessionPersona().getId().equals(event.getSender().getId())) {
            return;
        }

        // Search for a user commnd pattern
        Matcher mCmd = PAT_COMMAND.matcher(msgContent);
        if (mCmd.matches()) {
            userCommand(session, msgChannel, event, msgSender, mCmd);
            return;
        }

        // Search for a direct command pattern
        Matcher mAdmin = PAT_ADMIN.matcher(msgContent);
        if (mAdmin.matches()) {
            adminCommand(session, msgChannel, msgSender, mAdmin);
        }
    }

    /**
     * Manage the user commands
     *
     * @param session
     * @param msgChannel
     * @param msgSender
     * @param mCmd
     */
    private void userCommand(SlackSession session, SlackChannel msgChannel, SlackMessagePosted event,
            SlackUser msgSender, Matcher mCmd) {
        String command = mCmd.group(1).toUpperCase();
        String query = mCmd.group(2);

        LOG.info("Command '{}', query '{}'", command, query);

        switch (command) {
        case "SEARCH":
            botUpdateChannel(session, event, E_GREY_EXCLAMATION);
            BotStatistics.increment(StatCategory.SEARCH, msgSender.getUserName());
            commandSearch(session, msgChannel, query);
            break;
        case "GAME":
            botUpdateChannel(session, event, E_GREY_EXCLAMATION);
            BotStatistics.increment(StatCategory.GAME, msgSender.getUserName());
            commandGame(session, msgChannel, query);
            break;
        case "USER":
            botUpdateChannel(session, event, E_GREY_EXCLAMATION);
            BotStatistics.increment(StatCategory.USER, msgSender.getUserName());
            commandUser(session, msgChannel, query);
            break;
        case "COLL":
            botUpdateChannel(session, event, E_GREY_EXCLAMATION);
            BotStatistics.increment(StatCategory.COLLECTION, msgSender.getUserName());
            commandCollection(session, msgChannel, query);
            break;
        case "MEETUP":
            botUpdateChannel(session, event, E_GREY_EXCLAMATION);
            BotStatistics.increment(StatCategory.MEETUP, msgSender.getUserName());
            commandMeetup(session, msgChannel, query);
            break;
        case "HOT":
            botUpdateChannel(session, event, E_GREY_EXCLAMATION);
            BotStatistics.increment(StatCategory.HOT, msgSender.getUserName());
            commandHotList(session, msgChannel, query);
            break;
        default:
            LOG.info("Unknown command '" + command + "' found. Ignoring.");
        }
    }

    /**
     * Manage the admin commands
     *
     * @param session
     * @param msgChannel
     * @param msgSender
     * @param mAdmin
     */
    private void adminCommand(SlackSession session, SlackChannel msgChannel, SlackUser msgSender, Matcher mAdmin) {
        String command = mAdmin.group(1).toUpperCase();
        String params = mAdmin.group(2);

        if (msgSender.isAdmin()) {
            LOG.info("Command '{}' recieved from '{}' ({}) with params '{}'", command, msgSender.getUserName(),
                    msgSender.getId(), params);
            BotStatistics.writeFile();
            switch (command) {
            case "QUIT":
                adminQuitRestart(session, msgChannel, msgSender, true);
                break;
            case "RESTART":
                adminQuitRestart(session, msgChannel, msgSender, false);
                break;
            case "WELCOME":
                adminWelcome(params, session, msgChannel);
                break;
            default:
                LOG.info("Unknown command '{}' received from {}", command, msgSender.getUserName());
            }
        } else {
            session.sendMessageToUser(msgSender, "You are not authorised to use this command", null);
        }
    }

    /**
     * Quit or restart the bot
     *
     * @param session
     * @param channel
     * @param sender
     * @param quit
     */
    private void adminQuitRestart(SlackSession session, SlackChannel channel, SlackUser sender, boolean quit) {
        if (quit) {
            session.sendMessage(channel, "Bot will now quit :disappointed:");
        } else {
            session.sendMessage(channel, "Bot will now attempt to restart :relieved:");
        }

        // Update the stats
        BotStatistics.increment(StatCategory.ADMIN, sender.getUserName());

        // Quit or restart the bot
        com.omertron.slackbot.SlackBot.shutdown(quit ? ExitCode.QUIT : ExitCode.RESTART);
    }

    /**
     * Send out a welcome message.
     *
     * @param params
     * @param session
     * @param channel
     */
    private void adminWelcome(String params, SlackSession session, SlackChannel channel) {
        String user = StringUtils.trimToEmpty(params);
        if ("WHO".equalsIgnoreCase(user)) {
            BotWelcome.listUsers(session, channel);
            return;
        }

        SlackUser slackUser = session.findUserByUserName(user);
        if (slackUser == null) {
            session.sendMessage(channel, String.format("No user with username '%1$s' found", user));
        } else {
            session.sendMessage(channel, "Sending welcome message to " + slackUser.getUserName());
            BotWelcome.sendWelcomeMessage(session, channel, slackUser);
        }
    }

    /**
     * Search for a game
     *
     * @param session
     * @param msgChannel
     * @param msgSender
     * @param query
     */
    private void commandSearch(SlackSession session, SlackChannel msgChannel, String query) {
        StringBuilder response;
        SearchWrapper results;

        LOG.info("Search request for '{}'", query);
        session.sendTyping(msgChannel);

        try {
            results = BGG.searchBoardGame(query, false, false);
            LOG.info("Got {} results for '{}'", results.getTotal(), query);
        } catch (NullPointerException ex) {
            LOG.warn("Error getting BGG information: {}", ex.getMessage(), ex);
            session.sendMessage(msgChannel,
                    "Could not find information from BGG for " + query + ".\n Error: " + ex.getMessage());
            return;
        } catch (BggException ex) {
            LOG.warn("Error getting BGG information: {}", ex.getMessage(), ex);
            session.sendMessage(msgChannel,
                    "Could not get information from BGG for " + query + ".\n Error: " + ex.getMessage());
            return;
        }

        if (results.getItems() == null || results.getItems().isEmpty()) {
            session.sendMessage(msgChannel, "Could not find information from BGG for " + query);
            return;
        }

        response = new StringBuilder("Found ");
        response.append(results.getTotal()).append(" matches for *").append(query).append("*");

        List<SlackAttachment> attachments = new ArrayList<>();
        for (Thing item : results.getItems()) {
            attachments.add(makeSimpleAttachment(item));
        }

        SlackPreparedMessage spm = new SlackPreparedMessage.Builder().withMessage(response.toString())
                .withAttachments(attachments).withUnfurl(false).build();

        session.sendMessage(msgChannel, spm);

    }

    /**
     * Get information on a specific game
     *
     * @param session
     * @param msgChannel
     * @param msgSender
     * @param query
     */
    private void commandGame(SlackSession session, SlackChannel msgChannel, String query) {
        int bggId = 0;
        // If the query is a string, first search for the game
        if (!NumberUtils.isDigits(query)) {
            try {
                // Assume an exact search request
                SearchWrapper wrapper = BGG.searchBoardGame(query, true, false);
                if (wrapper.getTotal() > 0) {
                    bggId = wrapper.getItems().get(0).getId();
                }
            } catch (BggException ex) {
                LOG.warn("Failed to get exact search for {} from BGG", ex.getMessage(), ex);
                session.sendMessage(msgChannel, "Unable to find information on *'" + query + "'*");
                return;
            }
        } else {
            // Try converting the number
            bggId = NumberUtils.toInt(query, 0);
        }

        if (bggId > 0) {
            try {
                List<BoardGameExtended> results = BGG.getBoardGameInfo(bggId);
                if (results == null || results.isEmpty()) {
                    session.sendMessage(msgChannel, "No results found for BGG ID " + bggId);
                    return;
                }

                session.sendMessage(msgChannel, null, makeDetailedAttachment(results.get(0)));
            } catch (BggException ex) {
                LOG.warn("Failed to get information from BGG on game ID {} - Query '{}'", bggId, query, ex);
                session.sendMessage(msgChannel,
                        "Failed to get information from BGG on game ID " + bggId + " - Query '" + query + "'");
            }
        } else {
            session.sendMessage(msgChannel, "Unable to find information for game with ID *" + query + "*");
        }

    }

    /**
     * Get BGG information on a user
     *
     * @param msgChannel
     * @param msgSender
     * @param username
     */
    private void commandUser(SlackSession session, SlackChannel msgChannel, String username) {
        LOG.info("COMMAND: User information for '{}'", username);

        UserInfo result;
        try {
            result = BGG.getUserInfo(username);
        } catch (BggException ex) {
            LOG.warn("Failed to get user information on '{}'", username, ex);
            session.sendMessage(msgChannel, "Failed to get user information on " + username);
            return;
        }

        if (result == null) {
            session.sendMessage(msgChannel, "No information found for username '" + username + "'");
            return;
        }

        SlackAttachment sa = new SlackAttachment();
        sa.setFallback("User information on " + username);
        sa.setTitle(StringUtils.joinWith(" ", result.getFirstName(), result.getLastName()));
        sa.setTitleLink(Constants.BGG_LINK_USER + result.getName());
        sa.setAuthorIcon(formatHttpLink(result.getAvatarLink()));
        sa.setColor(Constants.ATTACH_COLOUR_GOOD);
        sa.addField("Year Registered", "" + result.getYearRegistered(), true);
        sa.addField("Location", StringUtils.joinWith(", ", result.getStateOrProvince(), result.getCountry()), true);
        sa.addField("Trade Rating", "" + result.getTradeRating(), true);
        sa.addField("Last Login", result.getLastLogin(), true);

        boolean first = true;
        StringBuilder sb = new StringBuilder();
        if (!result.getHotList().isEmpty()) {
            for (RankedList line : result.getHotList()) {
                if (first) {
                    first = false;
                } else {
                    sb.append("\n");
                }
                sb.append(String.format("%1$2d-%2$s (<https://boardgamegeek.com/boardgame/%3$d|%3$d>)",
                        line.getRank(), line.getName(), line.getId()));
            }
        }
        sa.addField("Hot List", sb.toString(), false);

        first = true;
        sb = new StringBuilder();
        if (!result.getTopList().isEmpty()) {
            for (RankedList line : result.getHotList()) {
                if (first) {
                    first = false;
                } else {
                    sb.append("\n");
                }
                sb.append(String.format("%1$2d-%2$s (<https://boardgamegeek.com/boardgame/%3$d|%3$d>)",
                        line.getRank(), line.getName(), line.getId()));
            }
        }
        sa.addField("Top List", sb.toString(), false);

        session.sendMessage(msgChannel, result.getName(), sa);
    }

    /**
     * Get the collection for a user
     *
     * @param session
     * @param msgChannel
     * @param params
     */
    private void commandCollection(SlackSession session, SlackChannel msgChannel, String params) {
        LOG.info("Getting collection information, params: '{}'", params);
        // Split the parameters out (if multiple)
        String username;
        String ids;
        Matcher m = PAT_COLL_PARAM.matcher(params);
        if (m.matches()) {
            username = m.group(1);
            ids = StringUtils.trimToNull(m.group(2));
        } else {
            username = params;
            ids = null;
        }

        LOG.info("  Username: '{}'", username);
        if (ids != null) {
            LOG.info("  ID(s): '{}'", ids);
        }

        CollectionItemWrapper result;
        try {
            List<IncludeExclude> includes = new ArrayList<>();
            List<IncludeExclude> excludes = new ArrayList<>();

            if (ids == null) {
                // Just get the username's owned collection
                includes.add(IncludeExclude.OWN);
            } else {
                // Get the individual ID items
                includes.add(IncludeExclude.STATS);
            }

            LOG.info("Getting collection information for '{}' with IDs '{}' & includes '{}'", username, ids,
                    includes);
            result = BGG.getCollectionInfo(username, ids, includes, excludes, false);
        } catch (BggException ex) {
            LOG.warn("Failed to get collection for user '{}'", username, ex);
            session.sendMessage(msgChannel, "Failed to get collection for user " + username);
            return;
        }

        if (result.getTotalItems() == 0) {
            String message = "No information found for username '" + username + "'";
            if (ids != null) {
                message += " with IDs '" + ids + "'";
            }
            session.sendMessage(msgChannel, message);
            return;
        } else {
            LOG.info("Found {} collection items for {}", result.getTotalItems(), username);
        }

        List<SlackAttachment> collList;
        if (ids == null) {
            LOG.info("Creating simple collection");
            collList = createSimpleCollection(session, msgChannel, result.getItems(), username);
        } else {
            LOG.info("Creating detailed collection");
            collList = createDetailedCollection(session, msgChannel, result.getItems(), username);
        }

        SlackPreparedMessage spm = new SlackPreparedMessage.Builder().withUnfurl(false).addAttachments(collList)
                .build();
        session.sendMessage(msgChannel, spm);
    }

    /**
     * Create a simple list of the collection items
     *
     * @param session
     * @param msgChannel
     * @param result
     * @param username
     * @return
     */
    private List<SlackAttachment> createSimpleCollection(SlackSession session, SlackChannel msgChannel,
            List<CollectionItem> result, String username) {
        int total = result.size();
        int perPart = 75;
        int totalParts = (total + perPart - 1) / perPart;
        int count = 0;
        int partCount = 1;

        String collectionFormat = "%1$s's collection - part %2$d of %3$d";

        LOG.info("\tMaking simple collection for {} with {} items and page size {}", username, total, perPart);
        session.sendMessage(msgChannel, username + " has " + total + " items in their collection. There will be "
                + totalParts + " parts listed.");

        SlackAttachment sa = new SlackAttachment();
        sa.setFallback(String.format(collectionFormat, username, partCount, totalParts));
        sa.setTitle(String.format(collectionFormat, username, partCount, totalParts));
        sa.setTitleLink(Constants.BGG_LINK_COLL + username);
        sa.setColor(Constants.ATTACH_COLOUR_GOOD);

        List<SlackAttachment> collList = new ArrayList<>();

        StringBuilder sb = new StringBuilder();
        for (CollectionItem item : result) {
            count++;
            if (count >= perPart) {
                sa.setText(sb.toString());
                collList.add(sa);

                count = 0;
                sb = new StringBuilder();
                sa = new SlackAttachment();
                partCount++;
                sa.setFallback(String.format(collectionFormat, username, partCount, totalParts));
                sa.setAuthorName(String.format(collectionFormat, username, partCount, totalParts));
                sa.setAuthorLink(Constants.BGG_LINK_COLL + username);
                sa.setColor(Constants.ATTACH_COLOUR_GOOD);
            }

            sb.append(String.format("%1$s (%2$s) - <%3$s%4$d|%4$d>\n", item.getName(), item.getYearPublished(),
                    Constants.BGG_LINK_GAME, item.getObjectId()));
        }
        sa.setText(sb.toString());
        collList.add(sa);

        LOG.info("\tCollection size of {} items and {} parts", result.size(), collList.size());
        return collList;
    }

    /**
     * Create a detailed view of of the collection
     *
     * @param session
     * @param msgChannel
     * @param result
     * @param username
     * @return
     */
    private List<SlackAttachment> createDetailedCollection(SlackSession session, SlackChannel msgChannel,
            List<CollectionItem> result, String username) {
        List<SlackAttachment> collList = new ArrayList<>();

        String logMessage, attMessage;
        switch (result.size()) {
        case 0:
            session.sendMessage(msgChannel, "No items found in the collection of " + username);
            return collList;
        case 1:
            logMessage = "Creating detailed attatchment for {} for {} item";
            attMessage = "Getting informaton on the item from the collection of " + username;
            break;
        default:
            logMessage = "Creating detailed attachments for {} for {} items";
            attMessage = "Getting informaton on the " + result.size() + " items from the collection of " + username;
        }

        LOG.info(logMessage, username, result.size());
        session.sendMessage(msgChannel, attMessage);
        result.forEach(game -> collList.add(createGameAttachment(game)));
        return collList;
    }

    /**
     * Format the collection item (game) into an attachment
     *
     * @param game
     * @return
     */
    private SlackAttachment createGameAttachment(CollectionItem game) {
        SlackAttachment sa = new SlackAttachment();
        String year = game.getYearPublished() == null ? UNKNOWN : " (" + game.getYearPublished() + ")";

        sa.setFallback(INFORMATION_ON + game.getName());
        sa.setTitle(game.getName() + year);
        sa.setTitleLink(Constants.BGG_LINK_GAME + game.getObjectId());
        sa.setAuthorIcon(game.getThumbnail());
        sa.setText(StringEscapeUtils.unescapeHtml4(game.getComment()));
        sa.setColor(Constants.ATTACH_COLOUR_GOOD);
        sa.setThumbUrl(formatHttpLink(game.getThumbnail()));
        sa.addField(BGG_ID, String.valueOf(game.getObjectId()), true);
        if (game.getStats() != null && game.getStats().getRating() != null) {
            float value = game.getStats().getRating().getValue();
            sa.addField("Rating", "" + (value > 0 ? value : "Not Rated"), true);
        }

        if (game.getNumPlays() > 0) {
            sa.addField("Num Plays", "" + game.getNumPlays(), true);
        }

        LOG.info("Owner Status: {}", game.getOwnerStatus().toString());

        List<String> status = calculateStatus(game.getOwnerStatus());
        if (!status.isEmpty()) {
            sa.addField("Owner Status", StringUtils.join(status, ","), true);
        }
        LOG.info("Status: {}", status.toString());
        return sa;
    }

    /**
     * Create a string list with the list of owner status fields
     *
     * @param ownerStatus The object to process
     * @return A list of the string statuses
     */
    private List<String> calculateStatus(OwnerStatus ownerStatus) {
        List<String> status = new ArrayList<>();
        if (ownerStatus != null) {
            if (ownerStatus.isOwn()) {
                status.add("Own");
            }
            if (ownerStatus.isForTrade()) {
                status.add("For Trade");
            }
            if (ownerStatus.isPreordered()) {
                status.add("Pre-ordered");
            }
            if (ownerStatus.isPreviouslyOwned()) {
                status.add("Prev Owned");
            }
            if (ownerStatus.isWant()) {
                status.add("Wanted");
            }
            if (ownerStatus.isWantToBuy()) {
                status.add("Want To Buy");
            }
            if (ownerStatus.isWantToPlay()) {
                status.add("Want To Play");
            }
        }
        return status;
    }

    /**
     * Make a simple attachment for listing multiple games
     *
     * @param game
     * @return
     */
    private SlackAttachment makeSimpleAttachment(Thing game) {
        StringBuilder nameFormatted = new StringBuilder(game.getName());
        if (game.getYearPublished() == null) {
            nameFormatted.append(UNKNOWN);
        } else {
            nameFormatted.append(" (").append(game.getYearPublished()).append(")");
        }
        nameFormatted.append(" ID: ").append(game.getId());

        SlackAttachment sa = new SlackAttachment();
        sa.setFallback(INFORMATION_ON + game.getPrimaryName());
        sa.setTitle(nameFormatted.toString());
        sa.setTitleLink(Constants.BGG_LINK_GAME + game.getId());
        sa.setColor(Constants.ATTACH_COLOUR_GOOD);

        return sa;
    }

    /**
     * Make a detailed attachment for a game
     *
     * @param game
     * @return
     */
    private SlackAttachment makeDetailedAttachment(BoardGameExtended game) {
        SlackAttachment sa = new SlackAttachment();
        String year = game.getYearPublished() == null ? UNKNOWN : " (" + game.getYearPublished() + ")";

        sa.setFallback(INFORMATION_ON + game.getName());
        sa.setAuthorName(game.getName() + year);
        sa.setAuthorLink(Constants.BGG_LINK_GAME + game.getId());
        sa.setAuthorIcon(game.getThumbnail());
        sa.setText(StringEscapeUtils.unescapeHtml4(game.getDescription()));
        sa.setColor(Constants.ATTACH_COLOUR_GOOD);
        sa.setThumbUrl(formatHttpLink(game.getThumbnail()));
        sa.addField(BGG_ID, String.valueOf(game.getId()), true);
        sa.addField("Player Count", game.getMinPlayers() + "-" + game.getMaxPlayers(), true);
        sa.addField("Playing Time", String.valueOf(game.getPlayingTime()), true);
        sa.addField("Designer(s)", formatIdValue(game.getBoardGameDesigner()), true);
        sa.addField("Categories", formatIdValue(game.getBoardGameCategory()), true);
        sa.addField("Mechanics", formatIdValue(game.getBoardGameMechanic()), true);

        return sa;
    }

    /**
     * Format the values from an IdValue list into a comma separated string
     *
     * @param listToFormat
     * @return
     */
    private String formatIdValue(List<IdValue> listToFormat) {
        StringBuilder result = new StringBuilder();
        boolean first = true;

        for (IdValue idv : listToFormat) {
            if (first) {
                first = false;
                result.append(idv.getValue());
            } else {
                result.append(", ").append(idv.getValue());
            }
        }
        return result.toString();
    }

    /**
     * Process the MeetUp details and present them
     *
     * @param session
     * @param msgChannel
     * @param query
     */
    private void commandMeetup(SlackSession session, SlackChannel msgChannel, String query) {
        int muQuantity = 1;
        boolean muDetailed = false;

        if (StringUtils.isNotBlank(query)) {
            LOG.info("Processing parameters");
            List<String> params = new ArrayList<>();
            if (query.contains(" ")) {
                params.addAll(Arrays.asList(query.split(" ")));
            } else {
                params.add(query);
            }

            LOG.info("Found {} parameters", params.size());

            for (String p : params) {
                if (StringUtils.isNumeric(p)) {
                    muQuantity = NumberUtils.toInt(p, 1);
                    continue;
                }

                if ("DETAILED".equalsIgnoreCase(p)) {
                    muDetailed = true;
                }
            }
        }

        LOG.info("Quantity: {}", muQuantity);
        LOG.info("Detailed: {}", muDetailed);

        try {
            Meetup.readMeetUp(muQuantity);
        } catch (ApiException ex) {
            LOG.warn("Failed to read data from meetup: {}", ex.getMessage(), ex);
            com.omertron.slackbot.SlackBot.messageAdmins(session,
                    "Failed to read data from meetup: " + ex.getMessage());
            return;
        }

        Meetup.getMeetupsDays(7, false);

        List<SlackAttachment> attach = Meetup.getMeetupsQty(muQuantity, muDetailed);
        SlackPreparedMessage preparedMessage = new SlackPreparedMessage.Builder().addAttachments(attach)
                .withMessage("These are the upcoming MeetUps:").build();
        session.sendMessage(msgChannel, preparedMessage);
    }

    /**
     * Get the Hot List and format it
     *
     * @param session Session
     * @param channel Channel
     * @param param ItemType String
     */
    private void commandHotList(SlackSession session, SlackChannel channel, String param) {
        HotItemType itemType = validateHotParam(param);
        LOG.info("Getting hot list for '{}'", itemType.toString());

        try {
            List<HotListItem> results = BGG.getHotItems(itemType);

            List<SlackAttachment> listAttach = new ArrayList<>();
            for (HotListItem item : results) {
                if (item.getRank() <= 10) {
                    listAttach.add(convertHotListToAttach(itemType, item));
                }
            }

            SlackPreparedMessage spm = new SlackPreparedMessage.Builder()
                    .withMessage("Hot List for " + itemType.toString()).addAttachments(listAttach).build();
            session.sendMessage(channel, spm);
        } catch (BggException ex) {
            LOG.info("Failed to get Hot List for {}", itemType.toString(), ex);
        }
    }

    /**
     * Format a HotListItem into a SlackAttachment
     *
     * @param listItem HotListItem to format
     * @return A SlackAttachment
     */
    private SlackAttachment convertHotListToAttach(HotItemType itemType, HotListItem listItem) {
        SlackAttachment sa = new SlackAttachment();

        sa.setColor(Constants.ATTACH_COLOUR_GOOD);
        sa.setThumbUrl(formatHttpLink(listItem.getThumbnail()));
        sa.setAuthorName("#" + listItem.getRank());

        if (listItem.getYearPublished() == null || listItem.getYearPublished() == 0) {
            sa.setTitle(listItem.getName());
        } else {
            sa.setTitle(String.format("%1$s (%2$d)", listItem.getName(), listItem.getYearPublished()));
        }

        switch (itemType) {
        case BOARDGAME:
            sa.setTitleLink(Constants.BGG_LINK_GAME + listItem.getId());
            break;
        case BOARDGAMECOMPANY:
            sa.setTitleLink(Constants.BGG_LINK_PUBLISHER + listItem.getId());
            break;
        case BOARDGAMEPERSON:
            sa.setTitleLink(Constants.BGG_LINK_DESIGNER + listItem.getId());
            break;
        default:
        }

        return sa;
    }

    /**
     * Validate the source string passed and convert to a HotItemType enum
     *
     * @param source Source string to convert
     * @return Matched HotItemType or default if unmatched
     */
    private HotItemType validateHotParam(String source) {
        LOG.info("Validating/Converting '{}' to a HotListItem");
        if (StringUtils.isBlank(source)) {
            return HotItemType.BOARDGAME;
        }

        if (StringUtils.equalsIgnoreCase("boardgame", source)) {
            return HotItemType.BOARDGAME;
        } else if (StringUtils.equalsIgnoreCase("person", source)) {
            return HotItemType.BOARDGAMEPERSON;
        } else if (StringUtils.equalsIgnoreCase("company", source)) {
            return HotItemType.BOARDGAMECOMPANY;
        } else {
            return HotItemType.BOARDGAME;
        }
    }

}