/**
* Copyright (c) 2005-2017, KoLmafia development team
* http://kolmafia.sourceforge.net/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* [1] Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* [2] Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* [3] Neither the name "KoLmafia" nor the names of its contributors may
* be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package net.sourceforge.kolmafia.chat;
import java.awt.Toolkit;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.session.ContactManager;
import net.sourceforge.kolmafia.utilities.StringUtilities;
public class ChatParser
{
private static final Pattern TABLECELL_PATTERN = Pattern.compile( "</?[tc].*?>" );
private static final Pattern PARENTHESIS_PATTERN = Pattern.compile( " \\(.*?\\)" );
private static final Pattern TITLE_PATTERN =
Pattern.compile( "<center><b>([^<]+)</b></center>" );
private static final Pattern WHO_PATTERN =
Pattern.compile( "<font color='?#?(\\w+)'?[^>]*>(.*?)</font></a>" );
private static final Pattern PLAYERID_PATTERN =
Pattern.compile( "showplayer\\.php\\?who\\=([-\\d]+)[\'\"][^>]*?>(.*?)</a>" );
// <span style='font-size:1.1em; font-weight: bold'><font color=green>[pvp]</font> <b><a target=mainpane href="showplayer.php?who=189466"><font color=black>scullyangel</font></b></a>: Hobo bosses don't count.<br></span>
// <font color=green>[clan]</font> <b><a target=mainpane href="showplayer.php?who=685853"><font color=black>Light Ninja</font></b></a>: Crap sorry. Could you send them each a terrarium too?<br>
// <span style='font-size:.9em; color:#444; font-family: Comic Sans, Comic Sans MS, cursive'><font color=green>[pvp]</font> <b><a target=mainpane href="showplayer.php?who=568742"><font color=black>kevbob</font></b></a>: what we need more of is science. totes.<br></span>
// A line can have an optional <span></span> surrounding it. The <br> comes BEFORE the </span>
// If you have multiple channels, there is a font surrounding that channel tag
// Player names get a <font>
private static final Pattern CHANNEL_PATTERN =
Pattern.compile( "(?:</span>)?(<span[^>]*>)?(?:<font color=[^>]*>)?(?:\\[([^\\]]*)\\])?(?:</font>)? ?(<i>)?(.*)" );
// <b><a target=mainpane href="showplayer.php?who=533033"><font color=black>Lord Kobel</font></a>:</b> yo<br>
// <b><a target=mainpane href="showplayer.php?who=1927238"><font color=black>TheLetterKay</font></b></a>: i need to script using maps to safety shelter grimace prime to get me dog hair pills<br><!--lastseen:1315078471-->
private static final Pattern SENDER_PATTERN =
Pattern.compile( "(?:<b>)(?:<b>)?<a target=mainpane href=\"showplayer\\.php\\?who=([-\\d]+)\">(?:<font[^>]*>)?(.*?)(?:</font>)?</[ab]>:?</[ab]>:? (.*)(?:</b>)?" );
private static final Pattern CHANNEL_LISTEN_PATTERN = Pattern.compile( " (.*?)<br>" );
public static void parseChannelList( final List<ChatMessage> newMessages, final String content )
{
Matcher channelMatcher = ChatParser.CHANNEL_LISTEN_PATTERN.matcher( content );
while ( channelMatcher.find() )
{
String channel = channelMatcher.group( 1 );
boolean isCurrentChannel = false;
if ( channel.indexOf( "<b" ) != -1 )
{
isCurrentChannel = true;
}
channel = "/" + KoLConstants.ANYTAG_PATTERN.matcher( channel ).replaceAll( "" ).trim();
if ( isCurrentChannel )
{
newMessages.add( new EnableMessage( channel, true ) );
}
else
{
newMessages.add( new EnableMessage( channel, false ) );
}
}
}
public static void parseContacts( final List<ChatMessage> newMessages, final String content, final boolean isClannies )
{
Matcher titleMatcher = TITLE_PATTERN.matcher( content );
String title = titleMatcher.find() ? titleMatcher.group( 1 ) : "Contacts Online";
Map<String, Boolean> contacts = new TreeMap<String, Boolean>();
Matcher whoMatcher;
if ( isClannies )
{
whoMatcher = PLAYERID_PATTERN.matcher( content );
}
else
{
whoMatcher = WHO_PATTERN.matcher( content );
}
while ( whoMatcher.find() )
{
String playerName = whoMatcher.group( 2 );
String color = isClannies ? "black" : whoMatcher.group( 1 );
boolean inChat = color.equals( "black" ) || color.equals( "blue" );
contacts.put( playerName, inChat ? Boolean.TRUE : Boolean.FALSE );
}
String noTableContent = ChatParser.TABLECELL_PATTERN.matcher( content ).replaceAll( "" );
String spacedContent = StringUtilities.singleStringReplace( noTableContent, "</b>", "</b> " );
WhoMessage message = new WhoMessage( contacts, spacedContent );
message.setHidden( Preferences.getBoolean( "useContactsFrame" ) );
newMessages.add( message );
ContactManager.updateContactList( title, contacts );
}
public static void parseChannel( final List<ChatMessage> chatMessages, final String content )
{
int startIndex = content.indexOf( ":" ) + 2;
int dotIndex = content.indexOf( "." );
String channel = content.substring( startIndex, dotIndex == -1 ? content.length() : dotIndex );
channel = "/" + KoLConstants.ANYTAG_PATTERN.matcher( channel ).replaceAll( "" );
if ( content.indexOf( "You are now talking in channel: " ) != -1 )
{
String currentChannel = ChatManager.getCurrentChannel();
chatMessages.add( new EnableMessage( channel, true ) );
chatMessages.add( new DisableMessage( currentChannel, true ) );
}
}
public static void parseSwitch( final List<ChatMessage> chatMessages, final String content )
{
int startIndex = content.indexOf( ":" ) + 2;
int dotIndex = content.indexOf( "." );
String channel = content.substring( startIndex, dotIndex == -1 ? content.length() : dotIndex );
channel = "/" + KoLConstants.ANYTAG_PATTERN.matcher( channel ).replaceAll( "" );
if ( content.indexOf( "You are now talking in channel: " ) != -1 )
{
chatMessages.add( new EnableMessage( channel, true ) );
}
}
public static void parseListen( final List<ChatMessage> chatMessages, final String content )
{
int startIndex = content.indexOf( ":" ) + 2;
int dotIndex = content.indexOf( "." );
String channel = content.substring( startIndex, dotIndex == -1 ? content.length() : dotIndex );
channel = "/" + KoLConstants.ANYTAG_PATTERN.matcher( channel ).replaceAll( "" );
ChatMessage message = null;
if ( content.indexOf( "Now listening to channel: " ) != -1 )
{
message = new EnableMessage( channel, false );
}
else if ( content.indexOf( "No longer listening to channel: " ) != -1 )
{
message = new DisableMessage( channel, false );
}
if ( message != null )
{
chatMessages.add( message );
}
}
public static List<ChatMessage> parseLines( final String content )
{
List<ChatMessage> chatMessages = new LinkedList<ChatMessage>();
ChatParser.parseLines( chatMessages, content );
return chatMessages;
}
public static void parseLines( final List<ChatMessage> chatMessages, final String content )
{
ChatParser.parsePlayerIds( content );
// There are no updates if there was a timeout.
if ( content == null || content.length() == 0 )
{
return;
}
String[] lines = ChatFormatter.formatInternalMessage( content ).split( "<br ?/?>" );
// Check for /haiku messages.
int nextLine = 0;
while ( nextLine < lines.length )
{
String line = lines[ nextLine ];
if ( line.length() == 0 )
{
++nextLine;
continue;
}
// HMC Radio message detection is expensive. Do it once per line.
HugglerMessage huggler = HugglerMessage.constructMessage( line );
if ( huggler != null )
{
++nextLine;
chatMessages.add( huggler );
continue;
}
StringBuilder currentLineBuilder = new StringBuilder( line );
while ( ++nextLine < lines.length )
{
line = lines[ nextLine ];
if ( line.length() == 0 )
{
continue;
}
if ( line.contains( "<a" ) )
{
break;
}
huggler = HugglerMessage.constructMessage( line );
if ( huggler != null )
{
break;
}
currentLineBuilder.append( "<br>" ).append( line );
}
ChatParser.parseLine( chatMessages, currentLineBuilder.toString().trim() );
if ( huggler != null )
{
++nextLine;
chatMessages.add( huggler );
}
}
}
private static void parseLine( final List<ChatMessage> chatMessages, String line )
{
// Empty messages do not need to be processed; therefore,
// return if one was retrieved.
if ( line == null )
{
return;
}
line = StringUtilities.globalStringDelete( line, "Invalid password submitted." ).trim();
if ( line.length() == 0 )
{
return;
}
if ( ChatParser.parseChannelMessage( chatMessages, line ) )
{
return;
}
line = ChatFormatter.removeMessageColors( line );
if ( line.contains( "(private):<" ) )
{
ChatParser.parsePrivateReceiveMessage( chatMessages, line );
return;
}
if ( line.contains( "<b>private to" ) )
{
ChatParser.parsePrivateSendMessage( chatMessages, line );
return;
}
ChatMessage message = new EventMessage( line, "green" );
chatMessages.add( message );
}
private static boolean parseChannelMessage( final List<ChatMessage> chatMessages, String line )
{
// If entire line is wrapped in a color - System Message, Mod Warning, and Mod Announcement - remove coloring
line = ChatFormatter.removeLineColor( line );
Matcher channelMatcher = ChatParser.CHANNEL_PATTERN.matcher( line );
if ( !channelMatcher.find() )
{
return false;
}
String span = channelMatcher.group( 1 );
String channel = channelMatcher.group( 2 );
boolean isAction = channelMatcher.group( 3 ) != null;
String content = channelMatcher.group( 4 );
if ( channel == null )
{
channel = ChatManager.getCurrentChannel();
}
else
{
channel = "/" + channel;
}
if ( isAction )
{
// Strip off the </i>
content = content.substring( 0, content.length() - 4 );
}
String playerId;
String playerName;
Matcher senderMatcher = ChatParser.SENDER_PATTERN.matcher( content );
if ( senderMatcher.lookingAt() )
{
playerId = senderMatcher.group( 1 ).trim();
playerName = senderMatcher.group( 2 ).trim();
content = senderMatcher.group( 3 );
}
else
{
return false;
}
if ( span != null )
{
// The </span> is already at the end of the content
content = span + content;
}
ChatMessage message;
if ( playerName.equals( "System Message" ) )
{
message = new SystemMessage( content );
}
else if ( playerName.equals( "Mod Warning" ) || playerName.equals( "Mod Announcement" ) )
{
message = new ModeratorMessage( channel, playerName, playerId, content );
}
else
{
ContactManager.registerPlayerId( playerName, playerId );
message = new ChatMessage( playerName, channel, content, isAction );
}
chatMessages.add( message );
return true;
}
private static void parsePrivateReceiveMessage( final List<ChatMessage> chatMessages, final String line )
{
String sender = line.substring( 0, line.indexOf( " (" ) );
sender = KoLConstants.ANYTAG_PATTERN.matcher( sender ).replaceAll( "" );
String recipient = KoLCharacter.getUserName();
String content = line.substring( line.indexOf( ":" ) + 9 ).trim();
if( Preferences.getBoolean( "chatBeep" ) )
{
Toolkit.getDefaultToolkit().beep();
}
ChatMessage message = new ChatMessage( sender, recipient, content, false );
chatMessages.add( message );
}
private static void parsePrivateSendMessage( final List<ChatMessage> chatMessages, final String line )
{
String sender = KoLCharacter.getUserName();
String recipient = line.substring( 0, line.indexOf( ":" ) );
recipient = KoLConstants.ANYTAG_PATTERN.matcher( recipient ).replaceAll( "" ).substring( 11 );
String content = line.substring( line.indexOf( ":" ) + 1 ).trim();
ChatMessage message = new ChatMessage( sender, recipient, content, false );
chatMessages.add( message );
}
public static final void parsePlayerIds( final String content )
{
if ( content == null )
{
return;
}
Matcher playerMatcher = ChatParser.PLAYERID_PATTERN.matcher( content );
String playerName, playerId;
while ( playerMatcher.find() )
{
playerName = KoLConstants.ANYTAG_PATTERN.matcher( playerMatcher.group( 2 ) ).replaceAll( "" );
playerName = ChatParser.PARENTHESIS_PATTERN.matcher( playerName ).replaceAll( "" );
playerName = playerName.replaceAll( ":", "" );
playerId = playerMatcher.group( 1 );
// Handle the new player profile links -- in
// this case, ignore the registration.
if ( !playerName.startsWith( "&" ) )
{
ContactManager.registerPlayerId( playerName, playerId );
}
}
}
}