();
+
+ public CompoundNegotiator(CapNegotiator.Listener... listeners) {
+ for (CapNegotiator.Listener listener : listeners) {
+ addListener(listener);
+ }
+ }
+
+ public void addListener(CapNegotiator.Listener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public boolean onNegotiateFeature(CapNegotiator capNegotiator, String feature) {
+ boolean more = false;
+ for (CapNegotiator.Listener listener : listeners) {
+ boolean keep = listener.onNegotiateFeature(capNegotiator, feature);
+ more = more || keep;
+ if (!keep)
+ listeners.remove(listener);
+ }
+ return more;
+ }
+
+ @Override
+ public boolean onNegotiateMissing(CapNegotiator capNegotiator, String feature) {
+ boolean more = false;
+ for (CapNegotiator.Listener listener : listeners) {
+ boolean keep = listener.onNegotiateMissing(capNegotiator, feature);
+ more = more || keep;
+ if (!keep)
+ listeners.remove(listener);
+ }
+ return more;
+ }
+
+ @Override
+ public boolean onNegotiateList(CapNegotiator capNegotiator, String[] features) throws IOException {
+ boolean more = false;
+ for (CapNegotiator.Listener listener : listeners) {
+ boolean keep = listener.onNegotiateList(capNegotiator, features);
+ more = more || keep;
+ if (!keep)
+ listeners.remove(listener);
+ }
+ return more;
+ }
+
+ @Override
+ public boolean onNegotiate(CapNegotiator capNegotiator, IrcPacket packet) throws IOException {
+ boolean more = false;
+ for (CapNegotiator.Listener listener : listeners) {
+ boolean keep = listener.onNegotiate(capNegotiator, packet);
+ more = more || keep;
+ if (!keep)
+ listeners.remove(listener);
+ }
+ return more;
+ }
+}
diff --git a/src/main/java/com/sorcix/sirc/cap/ServerTimeNegotiator.java b/src/main/java/com/sorcix/sirc/cap/ServerTimeNegotiator.java
new file mode 100644
index 0000000..e83a267
--- /dev/null
+++ b/src/main/java/com/sorcix/sirc/cap/ServerTimeNegotiator.java
@@ -0,0 +1,37 @@
+package com.sorcix.sirc.cap;
+
+import com.sorcix.sirc.IrcPacket;
+
+import java.io.IOException;
+
+/**
+ * @author pfnguyen
+ */
+public class ServerTimeNegotiator implements CapNegotiator.Listener {
+ @Override
+ public boolean onNegotiateFeature(CapNegotiator capNegotiator, String feature) {
+ return false;
+ }
+
+ @Override
+ public boolean onNegotiateMissing(CapNegotiator capNegotiator, String feature) {
+ return false;
+ }
+
+ @Override
+ public boolean onNegotiateList(
+ CapNegotiator capNegotiator, String[] features) throws IOException {
+ for (String feature : features) {
+ if (feature.contains("server-time")) {
+ capNegotiator.request(feature);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onNegotiate(CapNegotiator capNegotiator, IrcPacket packet) throws IOException {
+ return false;
+ }
+}
diff --git a/src/main/java/com/sorcix/sirc/event/BaseEvent.java b/src/main/java/com/sorcix/sirc/event/BaseEvent.java
new file mode 100644
index 0000000..fbdc24f
--- /dev/null
+++ b/src/main/java/com/sorcix/sirc/event/BaseEvent.java
@@ -0,0 +1,19 @@
+package com.sorcix.sirc.event;
+
+import com.sorcix.sirc.IrcConnection;
+import com.sorcix.sirc.IrcPacket;
+
+import java.util.Date;
+
+/**
+ * @author pfnguyen
+ */
+public class BaseEvent {
+ public final Date timestamp;
+ public final IrcConnection connection;
+
+ public BaseEvent(IrcConnection c, IrcPacket p) {
+ this.timestamp = p.getTimestamp();
+ connection = c;
+ }
+}
diff --git a/src/main/java/com/sorcix/sirc/event/MessageEventListener.java b/src/main/java/com/sorcix/sirc/event/MessageEventListener.java
new file mode 100644
index 0000000..5eee5c8
--- /dev/null
+++ b/src/main/java/com/sorcix/sirc/event/MessageEventListener.java
@@ -0,0 +1,137 @@
+/*
+ * MessageListener.java
+ *
+ * This file is part of the Sorcix Java IRC Library (sIRC).
+ *
+ * Copyright (C) 2008-2010 Vic Demuzere http://sorcix.com
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.sorcix.sirc.event;
+
+import com.sorcix.sirc.Channel;
+import com.sorcix.sirc.IrcConnection;
+import com.sorcix.sirc.IrcPacket;
+import com.sorcix.sirc.User;
+
+/**
+ * Notified of new IRC messages.
+ */
+public interface MessageEventListener {
+
+ public static class Action extends BaseEvent {
+ public final User sender;
+ public final Channel target;
+ public final String action;
+
+ public Action(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ action = p.getMessage().substring(7);
+ if (Channel.CHANNEL_PREFIX.indexOf(p.getArguments().charAt(0)) >= 0) {
+ // to channel
+ target = c.getState().getChannel(p.getArgumentsArray()[0]);
+ sender = target.updateUser(p.getSender(), true);
+ } else {
+ // to user
+ target = null;
+ sender = p.getSender();
+ }
+ }
+ }
+
+ public static class CtcpReply extends BaseEvent {
+ public final String command;
+ public final User sender;
+ public final String message;
+
+ public CtcpReply(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ final int cmdPos = p.getMessage().indexOf(' ');
+ command = p.getMessage().substring(0, cmdPos);
+ final String args = p.getMessage().substring(cmdPos + 1);
+ sender = p.getSender();
+ message = args;
+ }
+ }
+
+ public static class Message extends BaseEvent {
+ public final User sender;
+ public final Channel target;
+ public final String message;
+ public Message(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ message = p.getMessage();
+ if (Channel.CHANNEL_PREFIX.indexOf(
+ p.getArguments().charAt(0)) >= 0) {
+ target = c.getState().getChannel(p.getArgumentsArray()[0]);
+ sender = target.updateUser(p.getSender(), true);
+ } else {
+ target = null;
+ sender = p.getSender();
+ }
+ }
+ }
+
+ public static class Notice extends BaseEvent {
+ public final User sender;
+ public final Channel target;
+ public final String message;
+ public Notice(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ message = p.getMessage();
+ if (Channel.CHANNEL_PREFIX.indexOf(
+ p.getArguments().charAt(0)) >= 0) {
+ target = c.getState().getChannel(p.getArgumentsArray()[0]);
+ sender = target.updateUser(p.getSender(), true);
+ } else {
+ target = null;
+ sender = p.getSender();
+ }
+ }
+ }
+
+ /**
+ * Received an action in a channel.
+ */
+ void onAction(Action action);
+
+ /**
+ * Received a CTCP reply. Note that this event is only fired when
+ * receiving CTCP replies supported by sIRC. If you can't send a
+ * CTCP request, you won't get the reply.
+ */
+ void onCtcpReply(CtcpReply reply);
+
+ /**
+ * Received a message in a channel.
+ */
+ void onMessage(Message message);
+
+ /**
+ * Received a private notice.
+ */
+ void onNotice(Notice notice);
+
+ /**
+ * Received a private message.
+ */
+ void onPrivateMessage(Message message);
+}
diff --git a/src/main/java/com/sorcix/sirc/event/ServerEventListener.java b/src/main/java/com/sorcix/sirc/event/ServerEventListener.java
new file mode 100644
index 0000000..216872b
--- /dev/null
+++ b/src/main/java/com/sorcix/sirc/event/ServerEventListener.java
@@ -0,0 +1,248 @@
+/*
+ * ServerListener.java
+ *
+ * This file is part of the Sorcix Java IRC Library (sIRC).
+ *
+ * Copyright (C) 2008-2010 Vic Demuzere http://sorcix.com
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.sorcix.sirc.event;
+
+import com.sorcix.sirc.Channel;
+import com.sorcix.sirc.IrcConnection;
+import com.sorcix.sirc.IrcPacket;
+import com.sorcix.sirc.User;
+
+public interface ServerEventListener {
+
+ public static class Invite extends BaseEvent {
+ public final User sender;
+ public final User target;
+ public final Channel channel;
+
+ public Invite(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ sender = p.getSender();
+ target = new User(p.getArgumentsArray()[0], c);
+ channel = c.createChannel(p.getArgumentsArray()[1]);
+ }
+ }
+
+ public static class Join extends BaseEvent {
+ public final Channel channel;
+ public final User sender;
+
+ public Join(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ String channel;
+ if (p.hasMessage()) {
+ channel = p.getMessage();
+ } else {
+ channel = p.getArguments();
+ }
+ // someone joined a channel
+ if (p.getSender().isUs()) {
+ // if the sender joining the channel is the client
+ // we need to add it to the channel list.
+ c.getState().addChannel(new Channel(channel, c, true));
+ } else {
+ // add sender to channel list.
+ c.getState().getChannel(channel).addUser(p.getSender());
+ }
+ this.channel = c.getState().getChannel(channel);
+ this.sender = p.getSender();
+ }
+ }
+
+ public static class Kick extends BaseEvent {
+ public final Channel channel;
+ public final User sender;
+ public final User target;
+ public final String message;
+
+ public Kick(IrcConnection c, IrcPacket p) {
+ super(c, p);
+
+ final String[] data = p.getArgumentsArray();
+ final User kicked = new User(data[1], c);
+ final Channel channel = c.getState().getChannel(data[0]);
+ if (kicked.isUs()) {
+ // if the user leaving the channel is the client
+ // we need to remove it from the channel list
+ c.getState().removeChannel(data[0]);
+ } else {
+ // remove user from channel list.
+ channel.removeUser(kicked);
+ }
+ this.channel = channel;
+ this.sender = p.getSender();
+ this.target = kicked;
+ this.message = p.getMessage();
+ }
+ }
+
+ public static class Motd extends BaseEvent {
+ public final String motd;
+ public Motd(IrcConnection c, IrcPacket p, String motd) {
+ super(c, p);
+ this.motd = motd;
+ }
+ }
+ public static class Mode extends BaseEvent {
+ public final Channel channel;
+ public final User sender;
+ public final String mode;
+ public Mode(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ channel = c.getState().getChannel(p.getArgumentsArray()[0]);
+ sender = p.getSender();
+ mode = p.getArguments().substring(
+ p.getArgumentsArray()[0].length() + 1);
+ }
+ }
+
+ public static class Nick extends BaseEvent {
+ public final User oldUser;
+ public final User newUser;
+ public Nick(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ newUser = new User(
+ p.hasMessage() ? p.getMessage() : p.getArguments(), c);
+ oldUser = p.getSender();
+ }
+
+ }
+
+ public static class Part extends BaseEvent {
+ public final User sender;
+ public final Channel channel;
+ public final String message;
+
+ public Part(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ channel = c.getState().getChannel(p.getArguments());
+ sender = p.getSender();
+ message = p.getMessage();
+ }
+ }
+
+ public static class Quit extends BaseEvent {
+ public final User sender;
+ public final String message;
+
+ public Quit(IrcConnection c, IrcPacket p) {
+ super(c, p);
+ sender = p.getSender();
+ message = p.getMessage();
+ }
+ }
+
+ public static class Topic extends BaseEvent {
+ public final User sender;
+ public final Channel channel;
+ public final String topic;
+ public Topic(IrcConnection c, IrcPacket p) {
+ super(c, p);
+
+ if ("TOPIC".equals(p.getCommand())) {
+ final Channel chan = c.getState().getChannel(p.getArguments());
+ channel = chan;
+ sender = chan.updateUser(p.getSender(), false);
+ topic = p.getMessage();
+ } else if (p.getNumericCommand() == IrcPacket.RPL_TOPIC) {
+ channel = c.getState().getChannel(p.getArgumentsArray()[1]);
+ sender = null;
+ topic = p.getMessage();
+ } else {
+ channel = null;
+ sender = null;
+ topic = null;
+ }
+ }
+ }
+
+ /**
+ * Someone (possibly us) was invited into a channel.
+ */
+ void onInvite(Invite invite);
+
+ /**
+ * Someone (possibly us) joined a channel.
+ */
+ void onJoin(Join join);
+
+ /**
+ * Someone (possibly us) was kicked from a channel.
+ *
+ * Note: This method does NOT return a shared
+ * user object. That means that it isn't possible to retrieve the
+ * user prefix (or any modes).
+ *
+ */
+ void onKick(Kick kick);
+
+ /**
+ * Someone (possibly us) changed a channel mode.
+ */
+ void onMode(Mode mode);
+
+ /**
+ * Someone (possibly us) changed his nickname. Note that the
+ * {@code oldUser} can not be used to send messages, as that
+ * nickname no longer exists.
+ *
+ * Note: This method does NOT return a shared
+ * sender object. That means that it isn't possible to retrieve the
+ * sender prefix (or any modes).
+ *
+ */
+ void onNick(Nick nick);
+
+ /**
+ * Someone (possibly us) left a channel.
+ *
+ * Note: This method does NOT return a shared
+ * sender object. That means that it isn't possible to retrieve the
+ * sender prefix (or any modes).
+ *
+ */
+ void onPart(Part part);
+
+ /**
+ * Someone quit the IRC server.
+ */
+ void onQuit(Quit quit);
+
+ /**
+ * Someone (possibly us) changed the topic of a channel, or we
+ * joined a new channel and discovered the topic. The {@code
+ * sender} will be {@code null} when we discovered the topic after
+ * joining.
+ */
+ void onTopic(Topic topic);
+
+ void onMotd(Motd motd);
+
+ void onConnect(IrcConnection c);
+
+ void onDisconnect(IrcConnection c);
+}
diff --git a/src/test/java/com/sorcix/sirc/IrcPacketTest.java b/src/test/java/com/sorcix/sirc/IrcPacketTest.java
new file mode 100644
index 0000000..0ab6d09
--- /dev/null
+++ b/src/test/java/com/sorcix/sirc/IrcPacketTest.java
@@ -0,0 +1,78 @@
+package com.sorcix.sirc;
+
+import static org.mockito.Matchers.matches;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import static org.junit.Assert.*;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.Calendar;
+import java.util.Date;
+
+@RunWith(MockitoJUnitRunner.class)
+public class IrcPacketTest {
+ @Mock
+ IrcConnection connection;
+
+ @Test
+ public void capLs() {
+ IrcPacket p = new IrcPacket(":leguin.freenode.net CAP * LS :" +
+ "account-notify extended-join identify-msg multi-prefix sasl",
+ connection);
+ assertEquals("CAP", p.getCommand());
+ assertEquals(
+ "account-notify extended-join identify-msg multi-prefix sasl",
+ p.getMessage());
+ assertEquals("* LS", p.getArguments());
+ assertEquals("LS", p.getArgumentsArray()[1]);
+ }
+
+ @Test
+ public void capReqMultiPrefix() {
+ IrcPacket p = new IrcPacket(
+ ":irc.colosolutions.net CAP * ACK :multi-prefix", connection);
+ assertEquals("CAP", p.getCommand());
+ assertEquals("multi-prefix", p.getMessage());
+ assertEquals("* ACK", p.getArguments());
+ assertEquals("ACK", p.getArgumentsArray()[1]);
+ }
+
+ @Test
+ public void capNak() {
+ IrcPacket p = new IrcPacket(
+ ":irc.znc.in CAP unknown-nick NAK :server-time-iso", connection);
+ assertEquals("CAP", p.getCommand());
+ assertEquals("server-time-iso", p.getMessage());
+ assertEquals("unknown-nick NAK", p.getArguments());
+ assertEquals("NAK", p.getArgumentsArray()[1]);
+ }
+
+ @Test
+ public void createCapLs() {
+ IrcPacket p = IrcPacketFactory.createCAPLS();
+ assertEquals("CAP LS", p.getRaw());
+ }
+
+ @Test
+ public void welcomeNumeric() {
+ IrcPacket p = new IrcPacket("@time=2014-09-15T15:33:11.232Z :" +
+ "irc.choopa.net 001 pfn :" +
+ "Welcome to the EFNet Internet Relay Chat Network pfn",
+ connection);
+ Date ts = p.getTimestamp();
+ assertEquals(new Date(1410795191232l), p.getTimestamp());
+ assertEquals(1410795191232l, p.getTime());
+ Calendar c = Calendar.getInstance();
+ c.setTime(ts);
+ assertEquals(8, c.get(Calendar.MONTH));
+ // only works in GMT-7
+// assertEquals(8, c.get(Calendar.HOUR));
+ assertTrue(p.isNumeric());
+ assertEquals(1, p.getNumericCommand());
+ }
+
+}