package tzar.mafiabot.engine;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import tzar.mafiabot.gui.MafiaView;
public class Parser {
private static final Pattern pageOfPattern = Pattern.compile("Page\\s(\\d+)\\sof\\s(\\d+)"); //ie Page 1 of 2
private static final Pattern postNumberPattern = Pattern.compile(".*\\Wp=?(\\d+).*");
private static final Pattern commandsPattern = Pattern.compile("##\\s?\\w+[^#]*");
private static final Pattern dayNightHack = Pattern.compile("(?i)(day|night)\\s*(\\d+)");
private final String thread, posts, author, post_content, bold_commands, next_button, post_edit;
private HashSet<String> gms = new HashSet<String>();
private Actors actors;
private int day = 0, night = 0;
private boolean isNight = false;
private HashSet<String> delayedKill = new HashSet<String>();
private File cacheFile = null;
private boolean readingFromCache = false;
private Elements newPosts = new Elements();
private boolean hasPlayersList = false;
private MafiaView view = null;
public Parser(String thread, MafiaView view) {
this.thread = thread;
this.cacheFile = new File("MafiaBot-" + thread.hashCode() + ".cache");
this.view = view;
if (thread.contains("bluehell")) {
posts = "div.post_block";
author = "div.post_username";
post_content = "div.post";
bold_commands = "strong.bbc:not(.blockquote strong):not(strong .blockquote):matches(" + commandsPattern.pattern() + ")";
next_button = "a[rel=next]";
post_edit = "p.edit";
} else if (thread.contains("mlponies") || thread.contains("roundstable")) {
posts = "div.post";
author = "a[href^=./memberlist]";
post_content = "div.content";
bold_commands = "span[style$=bold]:not(blockquote span):matchesOwn(" + commandsPattern.pattern() + ")";
next_button = "a:matchesOwn(^Next$)"; // makes sure it matches exactly on "Next" so it doesn't go to "Next Topic"
post_edit = "div.notice";
} else if (thread.contains("eridanipony")) {
posts = "table.tablebg:has(div.postbody)";
author = "b.postauthor";
post_content = "div.postbody";
bold_commands = "strong:not(div.quotecontent strong):not(strong div.quotecontent):matches("+ commandsPattern.pattern() +")";
next_button = "a:matchesOwn(^Next$)";
post_edit = "span.gensmall";
} else {
posts = author = post_content = bold_commands = next_button = post_edit = null;
System.out.println("ERROR: The specified forum " + thread + " is not yet supported.");
view.parseCompleted();
}
}
public void start() {
readingFromCache = cacheFile.exists();
try {
// time the parse execution
long startTime = System.currentTimeMillis();
parse(thread);
long endTime = System.currentTimeMillis();
System.out.println("(Parse completed in " + (endTime - startTime) / 1000L + " seconds.)");
} catch (Exception e) {
e.printStackTrace();
System.out.println("An error has occured while parsing: " + e.toString());
if (readingFromCache)
System.out.println("Try deleting the cache and reparsing to see if this error occurs again.");
}
// parsing finished; display the vote and post count
if (actors != null) {
actors.printVoteCount("Current vote count " + (isNight ? "(Night " + night : "(Day " + day) + "):", hasPlayersList);
if (day > 0) {
actors.printPostCount(day);
}
}
if (day > 0) {
// write the new posts to the cache if the game has started
try {
if (newPosts.size() > 1) {
// don't add the last post to the cache in case of mafia edits (BHP)
newPosts.remove(newPosts.size() - 1);
FileWriter fstream = new FileWriter(cacheFile, true);
BufferedWriter out = new BufferedWriter(fstream);
// write the other new posts
for (Element post : newPosts) {
out.write(post.outerHtml());
}
out.close();
}
System.err.println("Added " + newPosts.size() + " new posts to the cache.");
} catch (Exception e) {
e.printStackTrace();
System.out.println("An error has occured while writing to the cache: " + e.toString());
System.out.println("Try deleting the cache and reparsing to see if this error occurs again.");
}
} else {
System.err.println("A cache file was not generated because the game has not yet started.");
}
view.parseCompleted();
}
private void parse(String url) throws Exception {
System.err.println(url);
//load the page
final Element thisThreadPage = (readingFromCache ? Jsoup.parse(cacheFile, null) : Jsoup.connect(url).timeout(20000).get());
// update the GUI progress bar
//Element progress = thisThreadPage.select("a[href=#]:matchesOwn("+pageOfPattern.pattern()+")").first();
final Element progress = thisThreadPage.select("a[href=#]:matches("+pageOfPattern.pattern()+"), td.nav:matches("+pageOfPattern.pattern()+")").first();
if (progress != null) {
System.out.printf("%n" + progress.text().replace("�", "") + "%n"); // Print out the current page
Matcher pageOfMatcher = pageOfPattern.matcher(progress.text());
if (pageOfMatcher.find()) {
double current = Double.parseDouble(pageOfMatcher.group(1));
double end = Double.parseDouble(pageOfMatcher.group(2));
//System.out.println(current + " " + end);
view.setProgress((int) (current / end * 100));
}
} else if (!readingFromCache) {
view.setProgress(100);
} else {
// successfully opened the cache file
view.setProgress(1);
}
// check to see if we are jumping posts
// this is used after reading the cache to jump to the next post in the thread
final Matcher postNumberMatcher = postNumberPattern.matcher(url);
// get every post
final Elements allThePostsOnThisPage = thisThreadPage.select(posts);
// parse each post (post = the entire post, including avatar, heading, and signature)
for (final Element post : allThePostsOnThisPage) {
if (view.isStopped()) {
// Stop execution if stopped button was pressed
break;
}
// check if jumping posts
if (postNumberMatcher.matches()) {
String currentPostURL = post.select("a[href~="+ postNumberPattern.pattern() + "]:not("+post_content+" a)").last().absUrl("href");
//System.out.println(currentPostURL);
Matcher currentPostNumberMatcher = postNumberPattern.matcher(currentPostURL);
if (currentPostNumberMatcher.matches() &&
Integer.parseInt(currentPostNumberMatcher.group(1)) <= Integer.parseInt(postNumberMatcher.group(1))) {
//System.out.println(currentPostNumberMatcher.group(1) + " " + postNumberMatcher.group(1));
// if the current post number <= post number in the URL, jump to the next post
continue;
}
}
//System.out.println(post.outerHtml()); // Displays the post HTML for debugging
// if reading from the thread and this post is not the last post in the thread, append it to the list of new posts
if (!readingFromCache) {
newPosts.add(post);
}
// store the new day number here to advance the day at the end of the post
int newDay = 0, newNight = 0;
final String poster = post.select(author).first().text().trim();
Element postLink = post.select("a[href~="+ postNumberPattern.pattern() + "]:not("+post_content+" a)").last();
postLink.setBaseUri(thread);
String postURL = postLink.absUrl("href");
final Element postContent = post.select(post_content).first();
final Elements allBoldTagsInThisPost = postContent.select(bold_commands);
// First poster is automatically granted GM permissions
if (gms.isEmpty()) {
addGM(poster);
}
// try to determine phase change if the GM does not use any of the ## commands at all
if (!hasPlayersList) {
if (gms.contains(poster)) {
// look at all elements in the post that contain "day \d+" or "night \d+"
next:
for (Element nextDay : postContent.getElementsMatchingOwnText(dayNightHack.pattern())) {
Matcher m = dayNightHack.matcher(nextDay.text());
while (m.find()) {
// a new phase was found
if (actors == null) {
// if there was no actors yet, create one
actors = new Actors();
}
System.err.println(nextDay.text());
int num = Integer.parseInt(m.group(2));
if (m.group(1).equalsIgnoreCase("day") && num > day) {
newDay = num;
} else if (m.group(1).equalsIgnoreCase("night") && num > night) {
newNight = num;
}
break next;
}
}
} else if (actors != null) {
actors.addPlayer(poster);
}
}
// find all the bold tags in this post
for (Element aBoldTag : allBoldTagsInThisPost) {
// search for multiple commands within the same bold tag
Matcher multipleCommands = commandsPattern.matcher(aBoldTag.text());
while (multipleCommands.find()) {
String action = multipleCommands.group().trim().replaceFirst("##\\s+", "##");
// split the input string into a array of two strings; element 1 is the ##command, element 2 contains the parameters
String[] tokens = action.split(" ", 2);
String command = tokens[0].toLowerCase();
String parameter = (tokens.length > 1 ? tokens[1] : null);
// Player commands
if (actors != null) {
if (command.equals("##vote") && parameter != null) {
/*
if (day > 0 && !hasPlayersList) {
actors.addPlayer(parameter);
}
*/
actors.vote(poster, parameter);
continue;
} else if (command.equals("##unvote")) {
actors.unvote(poster);
continue;
} else if (!gms.contains(poster)) {
//System.out.println(command);
System.out.println("(!) " + poster + " used action: " + action + " --> " + postURL);
continue;
}
} else if (!gms.contains(poster)) {
System.out.println("(!) Ignored action \"" + action + "\" from " + poster + " due to lack of ##players list.");
continue;
}
// GM commands
// possibly rewrite to switch statement?
if (gms.contains(poster)) {
if (parameter == null) {
// all commands with no required parameters go here
if (command.equals("##purgevotes")) {
actors.clearVotes();
} else {
System.out.println("(!) Error: No parameters were given for command " + command);
}
} else if (command.equals("##players") && !hasPlayersList) {
//System.out.printf("Players list found: ");
// get the html of the ##players list
TextNode t = TextNode.createFromEncoded(aBoldTag.html(), "aURI");
// make an array with each element containing a player name
String[] playerList = t.getWholeText().split("<br />\\s*");
// create the internal player list using the array
actors = new Actors(Arrays.copyOfRange(playerList, 1, playerList.length));
hasPlayersList = true;
} else if (command.equals("##gm")) {
addGM(parameter);
} else if (command.equals("##removegm")) {
removeGM(parameter);
} else if (command.matches("##((day)|(night))")) {
if (actors == null) {
System.out.println("A player list was not found! Grabbing players as we go along...");
actors = new Actors();
hasPlayersList = false;
} else {
hasPlayersList = true;
}
// End the current phase after all commands in this post have been resolved
if (command.equals("##day")) {
newDay = Integer.parseInt(parameter);
} else {
newNight = Integer.parseInt(parameter);
}
} else if (actors == null) {
//System.out.println("(!) Ignored action \"" + action + "\" from " + poster + " due to lack of ##players list.");
continue;
} else if (command.matches("##add(player)?")) {
actors.addPlayer(parameter);
} else if (command.matches("##add((metaclass)|(npc))")) {
actors.addNpc(parameter);
} else if (command.matches("##remove((player)|(metaclass)|(npc))?")) {
actors.removePlayer(parameter);
} else if (command.equals("##takevote")) {
actors.takeVote(parameter);
} else if (command.equals("##givevote")) {
actors.giveVote(parameter);
} else if (command.equals("##setvoteweight")) {
String[] params = parameter.split(" ", 2);
try {
int num = Integer.parseInt(params[0]);
actors.setVoteWeight(params[1], num);
} catch (NumberFormatException e) {
System.out.println("(!) Usage: ##setvoteweight <num> <player>");
}
} else if (command.equals("##setvotenum")) {
String[] params = parameter.split(" ", 2);
try {
int num = Integer.parseInt(params[0]);
actors.setVoteNum(params[1], num);
} catch (NumberFormatException e) {
System.out.println("(!) Usage: ##setvotenum <num> <player>");
}
} else if (command.equals("##strikevote")) {
actors.unvote(parameter);
} else if (command.equals("##pardon")) {
actors.pardon(parameter);
} else if (command.equals("##proxyvote")) {
String[] params = parameter.split(" ");
if (params.length == 2) {
actors.vote(params[0], params[1]);
} else if (params.length > 2) {
// TODO: need to handle case where voter/candidate names are multiple words (use quotation marks?)
actors.vote(params[0], params[1]);
} else {
System.out.println("(x) Proxyvote failed: Please specify someone to vote for.");
}
} else if (command.equals("##lynch")) {
delayedKill.add(parameter);
} else if (command.equals("##nk")) {
delayedKill.add(parameter);
} else if (command.equals("##tk") || command.equals("##kill")) {
String name = actors.kill(parameter, day);
if (name != null) {
System.out.println(name + " was killed.");
}
} else if (command.equals("##suicide")) {
String name = actors.kill(parameter, day);
if (name != null) {
System.out.println(name + " suicided.");
}
} else if (command.equals("##resurrect")) {
actors.resurrect(parameter);
} else if (command.equals("##nightposts")) {
// TODO: implement counting nightposts
} else {
System.out.println("(!) Unrecognized GM command: " + action + " --> " + postURL);
}
}
// next command in the same bold tag
}
// next command in another bold tag
}
if (day > 0) {
//System.err.println(postContent.ownText());
actors.addPost(poster, day, postContent.ownText().length());
}
if (newNight > 0) {
newPhase(false, newNight);
}
if (newDay > 0) {
newPhase(true, newDay);
}
if (!gms.contains(poster) && !post.select(post_edit).isEmpty()) {
// print out warning that the poster edited their post!
System.out.println("(!!) " + poster + " edited their post! --> " + postURL);
}
// next post
}
// check if the stop button has been pressed
if (!view.isStopped()) {
if (readingFromCache) {
// finished parsing the cache; load the thread to check for new posts
readingFromCache = false;
view.setProgress(2);
Element linkToLastPost = allThePostsOnThisPage.last().select("a[href~="+ postNumberPattern.pattern() + "]:not("+post_content+" a)").last();
linkToLastPost.setBaseUri(thread);
parse(linkToLastPost.absUrl("href"));
} else {
// parse the next page
// Get the button that opens the next page.
Element nextButton = thisThreadPage.select(next_button).last();
if (nextButton != null)
parse(nextButton.absUrl("href"));
}
}
// otherwise do nothing
}
private void delayedKill() {
for (String player : delayedKill) {
// added 1 to the day of death because the player was actually alive today
String name = actors.kill(player, day + 1);
if (name != null) {
if (!isNight) {
System.out.println(name + " was lynched.");
} else {
System.out.println(name + " was nightkilled.");
}
}
}
delayedKill.clear();
}
private void newPhase(boolean isDay, int num) {
actors.printVoteCount("End of " + (isNight ? "Night " + night : "Day " + day) + " vote count:", hasPlayersList);
delayedKill();
actors.clearVotes();
if (isDay) {
day = num;
isNight = false;
view.setPhase("Day " + day);
actors.printPlayers();
} else {
night = num;
isNight = true;
view.setPhase("Night " + num);
}
}
private void addGM(String name) {
if (gms.add(name)) {
System.out.println(name + " was granted GM permissions. Current GMs: " + gms.toString());
}
}
private void removeGM(String name) {
if (gms.remove(name)) {
System.out.println(name + " had their GM permissions revoked. Current GMs: " + gms.toString());
}
}
}