/*
This file is part of leafdigital leafChat.
leafChat 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
(at your option) any later version.
leafChat 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 leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 Samuel Marshall.
*/
package com.leafdigital.irc;
import java.util.*;
import com.leafdigital.irc.api.*;
import leafchat.core.api.*;
/** Parses incoming IRC text into messages */
public class IRCMessageParser implements MsgOwner
{
/** Message dispatcher */
private MessageDispatch mdp;
private PluginContext context;
/**
* @param context Context used to register events.
* @throws GeneralException Any error registering messages
*/
public IRCMessageParser(PluginContext context) throws GeneralException
{
this.context = context;
context.registerMessageOwner(this);
context.registerExtraMessageClass(UserSourceIRCMsg.class);
context.registerExtraMessageClass(UserIRCMsg.class);
context.registerExtraMessageClass(ChanIRCMsg.class);
context.registerExtraMessageClass(UserNoticeIRCMsg.class);
context.registerExtraMessageClass(UserModeIRCMsg.class);
context.registerExtraMessageClass(UserMessageIRCMsg.class);
context.registerExtraMessageClass(UserCTCPResponseIRCMsg.class);
context.registerExtraMessageClass(UserCTCPRequestIRCMsg.class);
context.registerExtraMessageClass(UserActionIRCMsg.class);
context.registerExtraMessageClass(UnknownIRCMsg.class);
context.registerExtraMessageClass(TopicIRCMsg.class);
context.registerExtraMessageClass(SilenceIRCMsg.class);
context.registerExtraMessageClass(ServerIRCMsg.class);
context.registerExtraMessageClass(ServerSendMsg.class);
context.registerExtraMessageClass(ServerRearrangeMsg.class);
context.registerExtraMessageClass(ServerNoticeIRCMsg.class);
context.registerExtraMessageClass(ServerLineMsg.class);
context.registerExtraMessageClass(ServerDisconnectedMsg.class);
context.registerExtraMessageClass(ServerConnectionFinishedMsg.class);
context.registerExtraMessageClass(ServerConnectedMsg.class);
context.registerExtraMessageClass(QuitIRCMsg.class);
context.registerExtraMessageClass(PingIRCMsg.class);
context.registerExtraMessageClass(PartIRCMsg.class);
context.registerExtraMessageClass(NumericIRCMsg.class);
context.registerExtraMessageClass(NickIRCMsg.class);
context.registerExtraMessageClass(KickIRCMsg.class);
context.registerExtraMessageClass(JoinIRCMsg.class);
context.registerExtraMessageClass(InviteIRCMsg.class);
context.registerExtraMessageClass(ErrorIRCMsg.class);
context.registerExtraMessageClass(ChanNoticeIRCMsg.class);
context.registerExtraMessageClass(ChanModeIRCMsg.class);
context.registerExtraMessageClass(ChanMessageIRCMsg.class);
context.registerExtraMessageClass(ChanCTCPRequestIRCMsg.class);
context.registerExtraMessageClass(ChanActionIRCMsg.class);
context.requestMessages(ServerLineMsg.class, this);
}
private static int find(byte b, byte[] bytes, int start)
{
for(int i=start; i<bytes.length; i++)
{
if(bytes[i] == b)
{
return i;
}
}
return -1;
}
/**
* Message processor called whenever a line of data is received from server.
* @param msg Server line message
* @throws GeneralException
*/
public void msg(ServerLineMsg msg) throws GeneralException
{
if(msg.isHandled())
{
return;
}
byte[] line = msg.getLine();
msg.markHandled();
int space;
// Get prefix if provided
byte[] prefix = null;
int pos = 0;
if(line[0] == ':')
{
space = find((byte)' ', line, 0);
if(space == -1)
{
IRCMsg im = new UnknownIRCMsg("No space in line");
im.init(msg.getServer(), line, null, null, new byte[][]{}, false);
setEncoding(im, im.getServer(), null, null);
dispatchMessage(im);
return;
}
prefix = new byte[space-1];
System.arraycopy(line, 1, prefix, 0, space-1);
pos = space+1;
}
// Get command
space = find((byte)' ', line, pos);
if(space == -1)
{
space = line.length;
}
byte[] command = new byte[space-pos];
System.arraycopy(line, pos, command, 0, command.length);
pos = space+1;
// Get params
boolean includesPostfix = false;
List<byte[]> params = new LinkedList<byte[]>();
while(pos < line.length)
{
if(line[pos] == ':') // Final parameter begins with : to include spaces
{
byte[] param = new byte[line.length-pos-1];
System.arraycopy(line, pos+1, param, 0, param.length);
params.add(param);
pos = line.length;
includesPostfix = true;
}
else
{
space = find((byte)' ', line, pos);
if(space == -1) space = line.length;
byte[] param = new byte[space-pos];
System.arraycopy(line, pos, param, 0, param.length);
params.add(param);
pos = space+1;
}
}
byte[][] paramsArray = params.toArray(new byte[0][]);
IRCMsg base = new IRCMsg();
base.init(msg.getServer(), line, prefix, command, paramsArray, includesPostfix);
base.setSequence(msg);
generateMessage(base);
}
/**
* Generates and dispatches an IRC message of specific subclass, based on
* the parameters in the given base message.
* @param base Base message containing all the parameters
*/
private void generateMessage(IRCMsg base)
{
IRCMsg msg;
try
{
String sCommand = base.getCommand();
if(sCommand.matches("[0-9]{3}"))
{
msg = genNumeric(base);
}
else if(sCommand.equals("NOTICE"))
{
msg = genNotice(base);
}
else if(sCommand.equals("PRIVMSG"))
{
msg = genPrivMsg(base);
}
else if(sCommand.equals("NICK"))
{
msg = genNick(base);
}
else if(sCommand.equals("QUIT"))
{
msg = genQuit(base);
}
else if(sCommand.equals("SILENCE"))
{
msg = genSilence(base);
}
else if(sCommand.equals("MODE"))
{
msg = genMode(base);
}
else if(sCommand.equals("INVITE"))
{
msg = genInvite(base);
}
else if(sCommand.equals("JOIN"))
{
msg = genJoin(base);
}
else if(sCommand.equals("PART"))
{
msg = genPart(base);
}
else if(sCommand.equals("TOPIC"))
{
msg = genTopic(base);
}
else if(sCommand.equals("KICK"))
{
msg = genKick(base);
}
else if(sCommand.equals("PING"))
{
msg = genPing(base);
}
else if(sCommand.equals("ERROR"))
{
msg = genError(base);
}
else
{
throw new InvalidMessageException("Failed to recognise command");
}
}
catch(InvalidMessageException ime)
{
msg = new UnknownIRCMsg(ime.getMessage());
}
// Add in base parameters and dispatch
msg.init(base);
if(!msg.hasEncoding())
// Allows code further down to set specific encoding first, if needed
{
if(msg instanceof ChanIRCMsg)
{
ChanIRCMsg fromChan = (ChanIRCMsg)msg;
setEncoding(msg, msg.getServer(), fromChan.getChannel(), fromChan.getSourceUser());
}
else if(msg instanceof UserSourceIRCMsg)
{
UserSourceIRCMsg fromUser = (UserSourceIRCMsg)msg;
setEncoding(msg, msg.getServer(), null, fromUser.getSourceUser());
}
else
{
setEncoding(msg, msg.getServer(), null, null);
}
}
dispatchMessage(msg);
}
private void setEncoding(IRCMsg msg, Server s, String chan, IRCUserAddress user)
{
IRCEncoding.EncodingInfo ei =
context.getSingle(IRCEncoding.class).getEncoding(s, chan, user);
msg.setEncoding(ei);
}
private IRCMsg genNumeric(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
byte[][] params = base.getParams();
if(params.length<1)
{
throw new InvalidMessageException("Missing target for server numeric");
}
String target = IRCMsg.convertISO(params[0]);
int numeric = Integer.parseInt(base.getCommand());
IRCMsg similar = null;
switch(numeric)
{
case NumericIRCMsg.RPL_CHANNELMODEIS:
if(params.length >= 3)
{
similar = genChanMode(
base.getServer(), null, params, IRCMsg.convertISO(params[1]), 1);
}
break;
}
NumericIRCMsg im = new NumericIRCMsg(
base.getPrefix(), target, numeric, similar);
return im;
}
private IRCMsg genNick(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
byte[][] params = base.getParams();
if(params.length<1)
{
throw new InvalidMessageException("Missing nick change");
}
return new NickIRCMsg(
new IRCUserAddress(base.getPrefix(), false),
IRCMsg.convertISO(params[0]));
}
private IRCMsg genSilence(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
byte[][] params = base.getParams();
if(params.length<1 || params[0].length<2)
{
throw new InvalidMessageException("Missing silence mask");
}
char flag = (char)params[0][0];
boolean positive = (flag == '+');
if(!positive && flag != '-')
{
throw new InvalidMessageException("Couldn't parse silence mask, expecting + or -");
}
return new SilenceIRCMsg(
new IRCUserAddress(base.getPrefix(), false),
positive,
IRCMsg.convertISO(params[0]).substring(1));
}
private IRCMsg genQuit(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
byte[][] params = base.getParams();
byte[] message = (params.length<1) ? null : params[0];
return new QuitIRCMsg(
new IRCUserAddress(base.getPrefix(), false), message);
}
private IRCMsg genNotice(IRCMsg base) throws InvalidMessageException
{
String prefix = base.getPrefix();
byte[][] params = base.getParams();
if(params.length<2)
{
throw new InvalidMessageException("Wrong number of params for NOTICE");
}
String target = IRCMsg.convertISO(params[0]);
byte[] text = params[1];
if(prefix == null || prefix.indexOf('!') == -1)
{
return new ServerNoticeIRCMsg(
prefix==null ? base.getServer().getReportedOrConnectedHost() : prefix,
target, text);
}
else
{
IRCUserAddress source = new IRCUserAddress(prefix, false);
// Status messages
if(target.length() >= 2 &&
base.getServer().getStatusMsg().indexOf(target.charAt(0)) != -1 &&
base.getServer().getChanTypes().indexOf(target.charAt(1)) != -1)
{
return new ChanNoticeIRCMsg(source, target.substring(1), target.charAt(0), text);
}
// Channel notices
else if(target.length() >= 1 && base.getServer().getChanTypes().indexOf(target.charAt(0)) != -1)
{
return new ChanNoticeIRCMsg(source, target, (char)0, text);
}
// User notices
else
{
if(text.length>2 && text[0] == 1 && text[text.length-1] == 1)
{
return genCTCP(source, target, true, false, text);
}
else
{
return new UserNoticeIRCMsg(source, target, text);
}
}
}
}
private IRCMsg genPrivMsg(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
String prefix = base.getPrefix();
byte[][] params = base.getParams();
if(params.length<2)
throw new InvalidMessageException("Wrong number of params for PRIVMSG");
String target = IRCMsg.convertISO(params[0]);
byte[] text = params[1];
IRCUserAddress source = new IRCUserAddress(prefix, false);
// Channel messages
if(target.length() >= 1 && base.getServer().getChanTypes().indexOf(target.charAt(0)) != -1)
{
if(text.length>2 && text[0] == 1)
{
return genCTCP(source, target, false, true, text);
}
else
{
return new ChanMessageIRCMsg(source, target, text);
}
}
// User messages
else
{
if(text.length>2 && text[0] == 1)
{
return genCTCP(source, target, false, false, text);
}
else
{
return new UserMessageIRCMsg(source, target, text);
}
}
}
private IRCMsg genCTCP(IRCUserAddress source, String target,
boolean response, boolean chan, byte[] text)
{
// Split into request and other bits
int space = 1; // Over initial char 1
for(;space<text.length;space++)
{
if(text[space] == 32 || text[space] == 1) break;
}
byte[] request = new byte[space-1];
byte[] after = new byte[Math.max(text.length-space-
(text[text.length-1] == 1 ? 2 : 1), 0)]; // Also remove ending char 1 if present, and space itself
System.arraycopy(text, 1, request, 0, request.length);
if(after.length>0)
{
System.arraycopy(text, space+1, after, 0, after.length);
}
String requestString = IRCMsg.convertISO(request);
if(response)
{
return new UserCTCPResponseIRCMsg(source, target, requestString, after);
}
else
{
if(requestString.equals("ACTION"))
{
if(chan)
{
return new ChanActionIRCMsg(source, target, after);
}
else
{
return new UserActionIRCMsg(source, target, after);
}
}
else
{
if(chan)
{
return new ChanCTCPRequestIRCMsg(source, target, requestString, after);
}
else
{
return new UserCTCPRequestIRCMsg(source, target, requestString, after);
}
}
}
}
private IRCMsg genMode(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
IRCUserAddress source = new IRCUserAddress(base.getPrefix(), false);
byte[][] params = base.getParams();
if(params.length<2)
{
throw new InvalidMessageException("Wrong number of params for MODE");
}
String target = IRCMsg.convertISO(params[0]);
// Channel modes
if(target.length() >= 1 && base.getServer().getChanTypes().indexOf(target.charAt(0)) != -1)
{
return genChanMode(base.getServer(), source, params, target, 0);
}
// User modes
else
{
String modes = IRCMsg.convertISO(params[1]);
return new UserModeIRCMsg(source, target, modes);
}
}
/**
* @param s Server for message
* @param source Source user (or null for the numeric type)
* @param params Message params
* @param chan Target channel
* @param offset 0 for a usual message, 1 for the numeric (has an extra param)
* @return Message
*/
private IRCMsg genChanMode(Server s, IRCUserAddress source, byte[][] params, String chan, int offset)
{
String[] modeParams = new String[params.length-(2+offset)];
for(int i = 0;i<modeParams.length;i++)
{
modeParams[i] = IRCMsg.convertISO(params[i+(2+offset)]);
}
boolean positive = true;
int nextParam = 0;
List<ChanModeIRCMsg.ModeChange> l = new LinkedList<ChanModeIRCMsg.ModeChange>();
String modes = IRCMsg.convertISO(params[1+offset]);
for(int i=0;i<modes.length();i++)
{
char c = modes.charAt(i);
if(c == '+')
{
positive = true;
}
else if(c == '-')
{
positive = false;
}
else
{
int modeType = s.getChanModeType(c);
String param = null;
switch(modeType)
{
case Server.CHANMODE_SETPARAM:
// If there's no param, break
if(!positive) break;
// Fall through
case Server.CHANMODE_USERSTATUS:
case Server.CHANMODE_ADDRESS:
case Server.CHANMODE_ALWAYSPARAM:
// There's a param, try to get it
if(modeParams.length > nextParam)
{
param = modeParams[nextParam++];
}
break;
case Server.CHANMODE_NOPARAM:
case Server.CHANMODE_UNKNOWN:
break;
default:
assert false;
}
l.add(new ChanModeIRCMsg.ModeChange(c, positive, param));
}
}
return new ChanModeIRCMsg(source, chan, modes, modeParams,
l.toArray(new ChanModeIRCMsg .ModeChange[l.size()]));
}
private IRCMsg genInvite(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
byte[][] params = base.getParams();
if(params.length<2)
{
throw new InvalidMessageException("Wrong number of params for INVITE");
}
return new InviteIRCMsg(
new IRCUserAddress(base.getPrefix(), false),
IRCMsg.convertISO(params[0]),
IRCMsg.convertISO(params[1]));
}
private IRCMsg genJoin(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
IRCUserAddress source = new IRCUserAddress(base.getPrefix(), false);
byte[][] params = base.getParams();
if(params.length<1)
{
throw new InvalidMessageException("Wrong number of params for JOIN");
}
String target = IRCMsg.convertISO(params[0]);
return new JoinIRCMsg(source, target);
}
private IRCMsg genPart(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
IRCUserAddress source = new IRCUserAddress(base.getPrefix(), false);
byte[][] params = base.getParams();
if(params.length<1)
{
throw new InvalidMessageException("Wrong number of params for PART");
}
String target = IRCMsg.convertISO(params[0]);
byte[] text = params.length >= 2 ? params[1] : null;
// Channel messages
return new PartIRCMsg(source, target, text);
}
private IRCMsg genTopic(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
IRCUserAddress source = new IRCUserAddress(base.getPrefix(), false);
byte[][] params = base.getParams();
if(params.length<1)
{
throw new InvalidMessageException("Wrong number of params for TOPIC");
}
String target = IRCMsg.convertISO(params[0]);
byte[] text = params.length >= 2 ? params[1] : new byte[0];
// Channel messages
return new TopicIRCMsg(source, target, text);
}
private IRCMsg genKick(IRCMsg base) throws InvalidMessageException
{
checkPrefix(base);
IRCUserAddress source = new IRCUserAddress(base.getPrefix(), false);
byte[][] params = base.getParams();
if(params.length<2)
{
throw new InvalidMessageException("Wrong number of params for KICK");
}
String target = IRCMsg.convertISO(params[0]);
String sVictim = IRCMsg.convertISO(params[1]);
byte[] text = params.length >= 3 ? params[2] : null;
// Channel messages
return new KickIRCMsg(source, target, sVictim, text);
}
private IRCMsg genPing(IRCMsg base) throws InvalidMessageException
{
byte[][] params = base.getParams();
String sCode = params.length >= 1 ? IRCMsg.convertISO(params[0]) : null;
return new PingIRCMsg(sCode);
}
private IRCMsg genError(IRCMsg base) throws InvalidMessageException
{
byte[][] params = base.getParams();
if(params.length<1)
{
throw new InvalidMessageException("Wrong number of params for ERROR");
}
return new ErrorIRCMsg(IRCMsg.convertISO(params[0]));
}
/**
* @param base Message to check
* @throws InvalidMessageException If there's no prefix
*/
private static void checkPrefix(IRCMsg base) throws InvalidMessageException
{
if(base.getPrefix() == null) throw new InvalidMessageException(
"Numerics must include prefix");
}
private void dispatchMessage(IRCMsg im)
{
if(mdp == null) // For testing mode
{
System.err.println(im);
}
else
{
mdp.dispatchMessageHandleErrors(im, false);
}
}
private static class InvalidMessageException extends GeneralException
{
InvalidMessageException(String sReason)
{
super(sReason);
}
}
// MessageOwner
@Override
public void init(MessageDispatch mdp)
{
this.mdp = mdp;
}
@Override
public String getFriendlyName()
{
return "IRC protocol messages received from server";
}
@Override
public Class<? extends Msg> getMessageClass()
{
return IRCMsg.class;
}
@Override
public boolean registerTarget(Object oTarget, Class<? extends Msg> cMessage,
MessageFilter mf, int iRequestID, int iPriority)
{
return true;
}
@Override
public void unregisterTarget(Object oTarget, int iRequestID)
{
}
@Override
public void manualDispatch(Msg m)
{
}
@Override
public boolean allowExternalDispatch(Msg m)
{
return false;
}
}