From 860d446d28776ec842fa53e8e08538d4e093d6e9 Mon Sep 17 00:00:00 2001 From: snowleo Date: Wed, 12 Oct 2011 03:14:07 +0200 Subject: EssentialsUpdate WIP --- .../src/f00f/net/irc/martyr/services/AutoJoin.java | 157 +++++++++++ .../net/irc/martyr/services/AutoReconnect.java | 271 +++++++++++++++++++ .../f00f/net/irc/martyr/services/AutoRegister.java | 289 +++++++++++++++++++++ .../net/irc/martyr/services/AutoResponder.java | 82 ++++++ 4 files changed, 799 insertions(+) create mode 100644 EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoJoin.java create mode 100644 EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoReconnect.java create mode 100644 EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoRegister.java create mode 100644 EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoResponder.java (limited to 'EssentialsUpdate/src/f00f/net/irc/martyr/services') diff --git a/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoJoin.java b/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoJoin.java new file mode 100644 index 000000000..786552a7b --- /dev/null +++ b/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoJoin.java @@ -0,0 +1,157 @@ +/* + * Original version: Ben Damm + * Changes by: Morgan Christiansson + * - Spotted bugs + * - Added timer + * - Responds to Invites + * - Re-tries a join with a bad key + * + * Note: Requires Java 1.4 + */ +package f00f.net.irc.martyr.services; + +import f00f.net.irc.martyr.GenericAutoService; +import f00f.net.irc.martyr.IRCConnection; +import f00f.net.irc.martyr.InCommand; +import f00f.net.irc.martyr.State; +import f00f.net.irc.martyr.TimerTaskCommand; +import f00f.net.irc.martyr.clientstate.Channel; +import f00f.net.irc.martyr.commands.InviteCommand; +import f00f.net.irc.martyr.commands.JoinCommand; +import f00f.net.irc.martyr.commands.KickCommand; +import f00f.net.irc.martyr.errors.GenericJoinError; + +/** + *

AutoJoin joins a group if the IRCConnection is ready. It will wait until + * it is ready if it is not (by waiting for the REGISTERED state change).

+ * + *

AutoJoin maintains a persistent Join (re-join if kicked). + * AutoJoin can cease to be persistent by calling the 'disable' + * method.

+ */ +public class AutoJoin extends GenericAutoService +{ + //static Logger log = Logger.getLogger(AutoJoin.class); + + private String channel = null; + private String key = null; + private TimerTaskCommand joinTimerTask = null; + private long joinTimerTaskDelay = 10*1000; + + public AutoJoin( IRCConnection connection, String channel ) + { + this( connection, channel, null ); + } + + public AutoJoin( IRCConnection connection, String channel, String key ) + { + super( connection ); + + this.channel = channel; + this.key = key; + + enable(); + + updateState( connection.getState() ); + } + + protected void updateState( State state ) + { + + if( state == State.REGISTERED ) + performJoin(); + } + + protected void updateCommand( InCommand command_o ) + { + if( command_o instanceof KickCommand ) + { + KickCommand kickCommand = (KickCommand)command_o; + + if( kickCommand.kickedUs( getConnection().getClientState() ) ) + { + if( Channel.areEqual(kickCommand.getChannel(), channel)) + { + performJoin(); + } + else + { + // mog: TODO: Should we really join a channel for which we aren't the AutoJoin:er? + // BD: You are quite right, this AutoJoin should only worry about itself. + // getConnection().sendCommand( new JoinCommand( kickCommand.getChannel() ) ); + } + } + } + else if(command_o instanceof GenericJoinError ) + { + GenericJoinError joinErr = (GenericJoinError)command_o; + + if( Channel.areEqual( joinErr.getChannel(), channel ) ) + { + //log.debug("AutoJoin: Failed to join channel: "+joinErr.getComment()); + scheduleJoin(); + } + } + else if( command_o instanceof InviteCommand ) + { + InviteCommand invite = (InviteCommand)command_o; + if(!getConnection().getClientState().isOnChannel(invite.getChannel())) + { + performJoin(); + } + } + } + + /** + * Sets up and sends the join command. + * */ + protected synchronized void performJoin() + { + setupJoin(); + sendJoinCommand(); + } + + /** + * Performs various tasks immediatly prior to sending a join command. + * Called from performJoin. + * */ + protected void setupJoin() + { + if(joinTimerTask != null) + { + joinTimerTask.cancel(); + joinTimerTask = null; + } + } + + /** + * This method sends the actual command. Called from performJoin. + * */ + protected void sendJoinCommand() + { + getConnection().sendCommand( new JoinCommand( channel, key ) ); + } + + protected void scheduleJoin() + { + if(joinTimerTask == null || !joinTimerTask.isScheduled()) + { + joinTimerTask = new TimerTaskCommand(getConnection(), new JoinCommand(channel, key)); + //TODO back off delay on repeated retries? + getConnection().getCronManager().schedule(joinTimerTask, joinTimerTaskDelay); + } + } + + public String toString() + { + if( key == null ) + return "AutoJoin [" + channel + "]"; + return "AutoJoin [" + channel + "," + key + "]"; + } + + // END AutoResponder +} + + + + diff --git a/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoReconnect.java b/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoReconnect.java new file mode 100644 index 000000000..06ff33f70 --- /dev/null +++ b/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoReconnect.java @@ -0,0 +1,271 @@ +package f00f.net.irc.martyr.services; + +import java.io.IOException; + +import f00f.net.irc.martyr.IRCConnection; +import f00f.net.irc.martyr.InCommand; +import f00f.net.irc.martyr.GenericAutoService; +import f00f.net.irc.martyr.State; +import f00f.net.irc.martyr.clientstate.ClientState; +import f00f.net.irc.martyr.commands.QuitCommand; + +/** + *

AutoReconnect performs the job of reconnecting to the server, if + * the connection is terminated unexpectedly. AutoReconnect + * will try to reconnect 5 times, and then give up. If AutoReconnect + * intercepts a QUIT command before the state change that is issued by + * us (bounced back from the server) then it will not try to + * reconnect.

+ * + *

Note that AutoReconnect has no play in nick negotiation, and as + * such, a failed nick negotiation does not count as a connection + * retry.

+ * + *

Testing note: Does a server send a QUIT before a forceful + * removal from the network? Should AutoReconnect not intercept + * QUITs? Certainly not all servers send QUITs even when you QUIT on + * your own; this class should be put into a command-out chain as well.

+ */ +public class AutoReconnect extends GenericAutoService +{ + //static Logger log = Logger.getLogger(AutoReconnect.class); + + private int attempt; + private int maxAttempts; + private int sleepTime; + private boolean disableOnQuit; + + // How many times to try reconnecting? + public static final int DEFAULT_MAX_ATTEMPTS = 5; + // TODO This is a rather simplistic method, personally I would like to + // see a version of this class that implements a backoff algorithm. + + // If we tried to connect, but failed, how long do we wait until we + // try again (ms)? + public static final int DEFAULT_CONNECT_SLEEPTIME = 1000; + + // If we get a QUIT command from the server notifying us that we have + // QUIT, then self-disable so that we don't reconnect. + public static final boolean DEFAULT_DISABLE_ON_QUIT = true; + + /** + * @param connection The IRCConnection + * @param maxAttempts The number of tries to do before giving up + * @param sleepBetween Milliseconds to sleep between retries + * @param disableOnQuit Automatically disable on quit event + */ + public AutoReconnect( IRCConnection connection, int maxAttempts, + int sleepBetween, boolean disableOnQuit ) + { + super( connection ); + + this.disableOnQuit = disableOnQuit; + this.maxAttempts = maxAttempts; + this.sleepTime = sleepBetween; + this.attempt = 0; + + enable(); + } + + public AutoReconnect( IRCConnection connection, int maxAttempts, + int sleepBetween ) + { + this( connection, maxAttempts, sleepBetween, DEFAULT_DISABLE_ON_QUIT ); + } + + /** + * Initializes with reasonable defaults. + * + * @param connection Connection we are associated with + */ + public AutoReconnect( IRCConnection connection ) + { + this( connection, DEFAULT_MAX_ATTEMPTS, DEFAULT_CONNECT_SLEEPTIME ); + } + + /** + * Attempts to connect, returning only when a connection has been made + * or repeated connections have failed. + * + * @param server Server to connect to + * @param port Port to connect to + * */ + public void go( String server, int port ) + { + doConnectionLoop( server, port ); + } + + /** + * Attempts a single connection to the server. The connection is done + * by asking the client state what server and port we are supposed to + * be connected to, and then calling IRCConnection.connect(...). + * + * @throws IOException if we could not connect successfully + * */ + protected void connect() + throws IOException + { + ClientState cstate = getConnection().getClientState(); + connect( cstate.getServer(), cstate.getPort() ); + } + + /** + * Attempts a single connection to the server. + * + * @param server Server to connect to + * @param port Port to connect to + * @throws IOException if we could not connect successfully + * */ + protected void connect( String server, int port ) + throws IOException + { + getConnection().connect( server, port ); + } + + protected void updateState( State state ) + { + //log.debug("AutoReconnect: Update with state " + state); + if( state == State.UNCONNECTED ) + { + // This should only happen if we were connected and then + // disconnected. Try connecting. + + // failedToConnect() prevents insane-reconnecting loop if + // we never registered, however, it introduces a delay if we + // were registered properly previously + if (failedToConnect(null)) + doConnectionLoop(); + } else if( state == State.REGISTERED ) { + this.attempt = 0; + } + + //log.debug("AutoReconnect: Returned from " + state); + } + + /** + * Calls connect() followed by failedToConnect(...) until a connection + * is made. Gets the server and port from the client state, implying + * that a connection was already attempted before. + * */ + protected void doConnectionLoop() + { + // Tell called proc to use client state info + doConnectionLoop( null, -1 ); + } + + /** + * Calls connect() followed by failedToConnect(...) until a connection + * is made, or the service is disabled. + * + * @param server Server to connect to + * @param port Port to connect to + * */ + protected void doConnectionLoop( String server, int port ) + { + boolean keeptrying = true; + + while( keeptrying && enabled ) + { + Exception error = null; + try + { + if( server == null ) + { + // Try getting the info from the client state + connect(); + } + else + { + connect( server, port ); + } + keeptrying = false; + } + catch( Exception e ) + { + error = e; + keeptrying = true; + } + + if( keeptrying ) + { + keeptrying = failedToConnect( error ); + } + } + } + + /** + * Called when the final failure has occurred. Default implementation + * does nothing. + * */ + protected void finalFailure() + { + //log.debug("AutoReconnect: Final failure."); + this.attempt = 0; + } + + /** + * Called when a failure to connect occurs. This method should do + * whatever needs to be done between connection attempts; usually this + * will be sleeping the current thread and checking if we should stop + * trying to reconnect. + * + * @param error Exception that caused the failure + * @return true if another attempt at connecting should occur + * */ + protected boolean failedToConnect( Exception error ) + { + //log.debug("AutoReconnect: Error connecting: " + error); + + this.attempt++; + + // abort if we've tried too many times + if( attempt >= maxAttempts ) + { + //log.debug("AutoReconnect: Tried " + attempt + " times, giving up."); + finalFailure(); + return false; + } + + // Well, try again damnit! + // Sleep for a bit first. + try + { + Thread.sleep( sleepTime ); + } + catch( InterruptedException ie ) + { + // And we're going to do what? + return false; + } + + return true; + } + + /** + * AutoReconnect will disable itself if it sees a quit command + * generated by returned from the server. This usually happens just + * before the server terminates the connection. Note that this would + * really be better if we could intercept messages on their way out, + * but Martyr doesn't do that. + */ + protected void updateCommand( InCommand command ) + { + if( disableOnQuit + && command instanceof QuitCommand + && ((QuitCommand)command).isOurQuit(getConnection().getClientState()) ) + { + //log.debug("AutoReconnect: Disabling due to receiving own QUIT."); + disable(); + } + } + + public String toString() + { + return "AutoReconnect [" + attempt + "]"; + } + +} + + + + diff --git a/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoRegister.java b/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoRegister.java new file mode 100644 index 000000000..f0cec08ba --- /dev/null +++ b/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoRegister.java @@ -0,0 +1,289 @@ +package f00f.net.irc.martyr.services; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import f00f.net.irc.martyr.IRCConnection; +import f00f.net.irc.martyr.InCommand; +import f00f.net.irc.martyr.GenericAutoService; +import f00f.net.irc.martyr.State; +import f00f.net.irc.martyr.TimerTaskCommand; +import f00f.net.irc.martyr.clientstate.ClientState; +import f00f.net.irc.martyr.commands.NickCommand; +import f00f.net.irc.martyr.commands.UserCommand; +import f00f.net.irc.martyr.commands.PassCommand; +import f00f.net.irc.martyr.errors.NickInUseError; +import f00f.net.irc.martyr.util.FullNick; +import java.util.logging.Logger; + +/** + *

AutoRegister performs the task of registering the user with the server + * once connected, including finding an appropriate nickname to use if the + * desired one is taken.

+ * + *

AutoRegister's default behaviour is to send the provided nickname. If it + * receives an ERR_NICKNAMEINUSE while unregistered, AutoRegister will try + * again, with an _ appended to the nick. If this fails five times, + * AutoRegister will ask the IRCConnection to disconnect(). Note that if it + * fails to connect it remains enabled, so that if IRCConnection.connect() is + * called, it will re-try the same 5 NICKs.

+ * + *

This default behaviour can be overridden by subclassing AutoRegister and + * overriding the getNickIterator( String baseNick ) method. It returns an + * instance of the java.util.Iterator interface which supplies nicknames (each + * object retreived from the Iterator is presumed to be a String). + * AutoRegister will iterate through the nickname list until there are no more + * items, at which point it will stop. For simple tasks such as providing a + * custom way to form new nicknames, overriding getNickIterator is + * sufficient.

+ * + *

AutoRegister will add itself as a state observer and as a command + * observer. It needs to receive the error.NickInUseError command so that + * it can re-try the registration, and it needs to detect when we + * transition into the UNREGISTERED state.

+ * + *

AutoRegister should be created before the IRCConnection.connect() + * is called. AutoRegister can be disabled by calling the 'disable()' + * method at any time. This simply removes AutoRegister as an + * observer for the state and commands.

+ * + */ +public class AutoRegister extends GenericAutoService +{ + static Logger log = Logger.getLogger(AutoRegister.class.getName()); + + // I've lost track of why the timer stuff was in here. I think the + // original purpose was to make AutoRegister take control of the nick + // more *after* registration occurred. This code is now causing so + // many problems *before* registration, that I think it may need to be + // pulled out. Maybe time to bring a "Manager" service into the + // fold? + private long nickTimerTaskDelay = 10*1000; + private TimerTaskCommand nickTimerTask; + + // Kept so it can be passed to getNickIterator() + private String originalNick; + // Used to set the client state once we register properly. + private String lastTryNick = null; + // Passed to the server on login + private String user; + private String name; + private String pass; + // Our list of nicks. + private Iterator nickIterator = null; + // attempt is only used for the debug output. + private int attempt = 0; + + public static final int MAX_ATTEMPTS = 5; + + public AutoRegister( IRCConnection connection, String nick, + String user, String name ) + { + super( connection ); + + this.originalNick = nick; + this.user = user; + this.name = name; + this.pass = null; + + enable(); + } + + public AutoRegister( IRCConnection connection, String nick, + String user, String name, String pass) + { + super( connection ); + + this.originalNick = nick; + this.user = user; + this.name = name; + this.pass = pass; + + enable(); + } + + + /** + *

This method supplies an Iterator that generates nicknames. Each successive + * failed attempt to login to the server with a nickname will be met with a new + * try using the next nickname in the iterator. When there are no more + * nicknames in the Iterator, AutoRegister gives up. Defining the Iterator as + * an anonymous class works well.

+ * + *

The iterator should iterate over String objects.

+ * + * @param baseNick The nickname passed into the constructor. + * @return Iterator over other attempts of nicks to try + */ + protected Iterator getNickIterator( final String baseNick ) + { + // This is simple and clean.. define the nick generation scheme as an + // anonymous class. + return new UnderscoreIterator(baseNick); + } + + private static class UnderscoreIterator implements Iterator + { + int count = 1; + String nick; + + public UnderscoreIterator( String base ) + { + this.nick = base; + } + + public boolean hasNext() + { + return count <= MAX_ATTEMPTS; + } + + public Object next() + { + if( hasNext() ) + { + String result = nick; + + // Set up the next round + nick = nick + "_"; + ++count; + + // return the value for this round. + return result; + } + else + { + throw new NoSuchElementException("No more nicknames"); + } + } + + public void remove() + { + throw new UnsupportedOperationException(); + } + } + + protected void updateState( State state ) + { + //log.debug("AutoRegister: Update with state " + state); + if( state == State.UNREGISTERED ) + { + // We need to do some registerin'! + nickIterator = getNickIterator( originalNick ); + attempt = 0; + doRegister(); + } + else if( state == State.REGISTERED ) + { + // We need to update the client state. + ClientState clientState = getConnection().getClientState(); + clientState.setNick( new FullNick( lastTryNick ) ); + clientState.setName( name ); + clientState.setUser( user ); + clientState.setPass( pass ); + } + + //log.debug("AutoRegister: Returned from " + state); + } + + protected void updateCommand( InCommand command ) + { + // First, check the state, because if we are already registered + // then none of this matters. + //if( getConnection().getState() == State.REGISTERED ) + //{ + // // We're registered. + // // No reason to continue. + // return; + //} + + if( command instanceof NickInUseError) + { + // If we get an error, then try another nick + NickInUseError nickErr = (NickInUseError)command; + if(nickErr.getNick().getNick().equals(originalNick)) + { + cancelNickAttempt(); // We don't want more than one of these + + scheduleNickAttempt(); + } + if(getConnection().getState() == State.UNREGISTERED ) + { + // re-register. + doRegister(); + } + } + else if( command instanceof NickCommand ) + { + // If we get a nick... then cancel a pending change + NickCommand nickCmd = (NickCommand)command; + if( nickCmd.getOldNick().equalsIgnoreCase( originalNick ) ) + { + cancelNickAttempt(); + } + } + } + + /** + * + */ + private void scheduleNickAttempt() + { + if( getConnection().getState().equals(State.REGISTERED)) + { + // We're already connected. + // We're short-circuiting + return; + } + if(nickTimerTask == null || !nickTimerTask.isScheduled()) + { + nickTimerTask = new TimerTaskCommand(getConnection(), new NickCommand(originalNick)); + //TODO back off delay on repeated retries? + getConnection().getCronManager().schedule(nickTimerTask, nickTimerTaskDelay); + } + } + + private void cancelNickAttempt() + { + if(nickTimerTask != null && nickTimerTask.isScheduled()) + { + nickTimerTask.cancel(); + } + } + + private void doRegister() + { + if( getConnection().getState() != State.UNREGISTERED ) + { + log.severe("AutoRegister: Tried to register but we are not UNREGISTERED"); + return; + } + + if( ! nickIterator.hasNext() ) + { + log.info("AutoRegister: Failed to register."); + getConnection().disconnect(); + return; + } + + lastTryNick = (String)nickIterator.next(); + ++attempt; + log.info("AutoRegister: Trying to register as " + lastTryNick); + + if (pass != null) { + getConnection().sendCommand( new PassCommand( pass )); + } + getConnection().sendCommand( new NickCommand( lastTryNick ) ); + getConnection().sendCommand( new UserCommand( user, name, getConnection() ) ); + } + + public String toString() + { + return "AutoRegister [" + attempt + "]"; + } + + // END AutoRegister +} + + + + diff --git a/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoResponder.java b/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoResponder.java new file mode 100644 index 000000000..08f3a7f29 --- /dev/null +++ b/EssentialsUpdate/src/f00f/net/irc/martyr/services/AutoResponder.java @@ -0,0 +1,82 @@ +package f00f.net.irc.martyr.services; + +import java.util.Observable; +import java.util.Observer; + +import f00f.net.irc.martyr.IRCConnection; +import f00f.net.irc.martyr.commands.ChannelModeCommand; +import f00f.net.irc.martyr.commands.JoinCommand; +import f00f.net.irc.martyr.commands.PingCommand; +import f00f.net.irc.martyr.commands.PongCommand; + +/** + * AutoResponder is where commands that should be auto-responded (such + * as PING-PONG) should go. + */ +public class AutoResponder implements Observer +{ + + private IRCConnection connection; + private boolean enabled = false; + + public AutoResponder( IRCConnection connection ) + { + this.connection = connection; + enable(); + } + + public void enable() + { + if( enabled ) + return; + + connection.addCommandObserver( this ); + enabled = true; + } + + public void disable() + { + if( !enabled ) + return; + + connection.removeCommandObserver( this ); + enabled = false; + } + + /** + * Does the work of figuring out what to respond to. + * If a PING is received, send a PONG. If we JOIN a channel, send a + * request for modes. + * */ + public void update( Observable observer, Object updated ) + { + + if( updated instanceof PingCommand ) + { + // We need to do some pongin'! + PingCommand ping = (PingCommand)updated; + + String response = ping.getPingSource(); + + connection.sendCommand( new PongCommand( response ) ); + } + else if( updated instanceof JoinCommand ) + { + // Determine if we joined, and if we did, trigger a MODE discovery + // request. + JoinCommand join = (JoinCommand)updated; + + if( join.weJoined( connection.getClientState() ) ) + { + connection.sendCommand( + new ChannelModeCommand( join.getChannel() ) ); + } + } + } + + // END AutoResponder +} + + + + -- cgit v1.2.3