View Javadoc

1   /*------------------------------------------------------------------------------
2    * The contents of this file are subject to the Mozilla Public License Version
3    * 1.1 (the "License"); you may not use this file except in compliance with
4    * the License. You may obtain a copy of the License at
5    * http://www.mozilla.org/MPL/
6    * Software distributed under the License is distributed on an "AS IS" basis,
7    * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
8    * the specific language governing rights and limitations under the License.
9    *
10   * The Original Code is levelonelabs.com code.
11   * The Initial Developer of the Original Code is Level One Labs. Portions
12   * created by the Initial Developer are Copyright (C) 2001 the Initial
13   * Developer. All Rights Reserved.
14   *
15   *         Contributor(s):
16   *             Scott Oster      (ostersc@alum.rpi.edu)
17   *             Steve Zingelwicz (sez@po.cwru.edu)
18   *             William Gorman   (willgorman@hotmail.com)
19   *
20   * Alternatively, the contents of this file may be used under the terms of
21   * either the GNU General Public License Version 2 or later (the "GPL"), or
22   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
23   * in which case the provisions of the GPL or the LGPL are applicable
24   * instead of those above. If you wish to allow use of your version of this
25   * file only under the terms of either the GPL or the LGPL, and not to allow
26   * others to use your version of this file under the terms of the NPL, indicate
27   * your decision by deleting the provisions above and replace them with the
28   * notice and other provisions required by the GPL or the LGPL. If you do not
29   * delete the provisions above, a recipient may use your version of this file
30   * under the terms of any one of the NPL, the GPL or the LGPL.
31   *----------------------------------------------------------------------------*/
32  
33  package com.levelonelabs.aim;
34  
35  import java.io.BufferedOutputStream;
36  import java.io.BufferedReader;
37  import java.io.DataInputStream;
38  import java.io.DataOutputStream;
39  import java.io.IOException;
40  import java.io.InterruptedIOException;
41  import java.io.StringReader;
42  import java.net.InetAddress;
43  import java.net.Socket;
44  import java.net.UnknownHostException;
45  import java.util.ArrayList;
46  import java.util.Arrays;
47  import java.util.Date;
48  import java.util.HashMap;
49  import java.util.HashSet;
50  import java.util.Iterator;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Set;
54  import java.util.StringTokenizer;
55  import java.util.Timer;
56  import java.util.TimerTask;
57  import java.util.logging.Logger;
58  
59  
60  /***
61   * Implements the AIM protocol
62   * 
63   * @author Scott Oster, Will Gorman
64   * @created September 4, 2001
65   */
66  public class AIMClient implements Runnable, AIMSender {
67      // check connection ever "TIME_DELAY" milliseconds (5 mins)
68      private static final long TIME_DELAY = 5 * 60 * 1000;
69      private static final String PING = "PING";
70  
71      private AimConnectionCheck watchdogCheck;
72      private AimConnectionCheck watchdogVerify;
73      boolean connectionVerified = false;
74      private Timer connectionCheck = new Timer();
75  
76      static Logger logger = Logger.getLogger(AIMClient.class.getName());
77  
78      // rate limiting
79      private static final int MAX_POINTS = 10;
80  
81      private static final int RECOVER_RATE = 2200;
82  
83      // for TOC3 (using toc2_login)
84      // private static final String REVISION = "\"TIC://Revision: 1.61 \" 160 US
85      // \"\" \"\" 3 0 30303 -kentucky -utf8 94791632";
86      private static final String REVISION = "\"TIC:TOC2\" 160";
87  
88      private String loginServer = "toc.oscar.aol.com";
89  
90      private int loginPort = 5190;
91  
92      private String authorizerServer = "login.oscar.aol.com";
93  
94      private int authorizerPort = 29999;
95  
96      private List aimListeners = new ArrayList();
97  
98      String name;
99  
100     private String pass;
101 
102     private String info;
103 
104     private String nonUserResponse;
105 
106     boolean online;
107 
108     private boolean autoAddUsers = false;
109 
110     // private final String ROAST = "Tic/Toc";
111     private int seqNo;
112 
113     private Socket connection;
114 
115     private DataInputStream in;
116 
117     private DataOutputStream out;
118 
119     private Map buddyHash;
120 
121     private int sendLimit = MAX_POINTS;
122 
123     private long lastFrameSendTime = System.currentTimeMillis();
124 
125     private int permitMode = PERMIT_ALL;
126 
127     private Set permitted;
128 
129     private Set denied;
130 
131 
132     /***
133      * Constructor for the AIMClient object
134      * 
135      * @param name
136      * @param pass
137      * @param info
138      *            Description of the Parameter
139      * @param response
140      *            what to say to non-users when they message the bot (if
141      *            autoaddUsers==false)
142      * @param autoAddUsers
143      */
144     public AIMClient(String name, String pass, String info, String response, boolean autoAddUsers) {
145         this.nonUserResponse = response;
146 
147         buddyHash = new HashMap();
148         permitted = new HashSet();
149         denied = new HashSet();
150         this.name = imNormalize(name);
151         this.pass = pass;
152         this.info = info;
153         this.autoAddUsers = autoAddUsers;
154         this.addBuddy(new AIMBuddy(name));
155     }
156 
157 
158     /***
159      * Constructor for the AIMClient object
160      * 
161      * @param name
162      * @param pass
163      * @param info
164      *            Description of the Parameter
165      * @param autoAddUsers
166      */
167     public AIMClient(String name, String pass, String info, boolean autoAddUsers) {
168         this(name, pass, info, "Sorry, you must be a user of this system to send requests.", autoAddUsers);
169     }
170 
171 
172     /***
173      * Constructor for the AIMClient object
174      * 
175      * @param name
176      * @param pass
177      * @param info
178      *            Description of the Parameter
179      */
180     public AIMClient(String name, String pass, String info) {
181         this(name, pass, info, false);
182     }
183 
184 
185     /***
186      * Constructor for the AIMClient object
187      * 
188      * @param name
189      * @param pass
190      */
191     public AIMClient(String name, String pass) {
192         this(name, pass, "No info", false);
193     }
194 
195 
196     /***
197      * Strip out HTML from a string
198      * 
199      * @param line * *
200      * @return the string without HTML
201      */
202     public static String stripHTML(String line) {
203         StringBuffer sb = new StringBuffer(line);
204         String out = "";
205 
206         for (int i = 0; i < (sb.length() - 1); i++) {
207             if (sb.charAt(i) == '<') {
208                 // Most tags
209                 if ((sb.charAt(i + 1) == '/') || ((sb.charAt(i + 1) >= 'a') && (sb.charAt(i + 1) <= 'z'))
210                     || ((sb.charAt(i + 1) >= 'A') && (sb.charAt(i + 1) <= 'Z'))) {
211                     for (int j = i + 1; j < sb.length(); j++) {
212                         if (sb.charAt(j) == '>') {
213                             sb = sb.replace(i, j + 1, "");
214                             i--;
215                             break;
216                         }
217                     }
218                 } else if (sb.charAt(i + 1) == '!') {
219                     // Comments
220                     for (int j = i + 1; j < sb.length(); j++) {
221                         if ((sb.charAt(j) == '>') && (sb.charAt(j - 1) == '-') && (sb.charAt(j - 2) == '-')) {
222                             sb = sb.replace(i, j + 1, "");
223                             i--;
224                             break;
225                         }
226                     }
227                 }
228             }
229         }
230 
231         out = sb.toString();
232         return out;
233     }
234 
235 
236     /***
237      * Protocol method
238      * 
239      * @para * *
240      * @return roasted string
241      */
242     private static String imRoast(String pass) {
243         String roast = "Tic/Toc";
244         String out = "";
245         String in = pass;
246         String out2 = "0x";
247         for (int i = 0; i < in.length(); i++) {
248             out = java.lang.Long.toHexString(in.charAt(i) ^ roast.charAt(i % 7));
249             if (out.length() < 2) {
250                 out2 = out2 + "0";
251             }
252 
253             out2 = out2 + out;
254         }
255 
256         return out2;
257     }
258 
259 
260     /***
261      * Protocol method * in
262      * 
263      * @return normalized string
264      */
265     private static String imNormalize(String in) {
266         String out = "";
267         in = in.toLowerCase();
268         char[] arr = in.toCharArray();
269         for (int i = 0; i < arr.length; i++) {
270             if (arr[i] != ' ') {
271                 out = out + "" + arr[i];
272             }
273         }
274 
275         return out;
276     }
277 
278 
279     /***
280      * Retrieve a buddy from the list
281      * 
282      * @param buddyName
283      * @return The buddy
284      */
285     public AIMBuddy getBuddy(String buddyName) {
286         return (AIMBuddy) buddyHash.get(imNormalize(buddyName));
287     }
288 
289 
290     /***
291      * Get an iterator for all the current buddy names
292      * 
293      * @return iterator
294      */
295     public Iterator getBuddyNames() {
296         return Arrays.asList(buddyHash.keySet().toArray()).iterator();
297     }
298 
299 
300     /***
301      * Sign on to aim server
302      */
303     public void signOn() {
304         new Thread(this).start();
305 
306         // check the connection
307         watchdogCheck = new AimConnectionCheck(this, true);
308 
309         // verify the message was received 5 secs later
310         watchdogVerify = new AimConnectionCheck(this, false);
311         connectionCheck.scheduleAtFixedRate(watchdogCheck, TIME_DELAY, TIME_DELAY);
312         connectionCheck.scheduleAtFixedRate(watchdogVerify, TIME_DELAY + 5000, TIME_DELAY);
313 
314         // give the server time to log us on before returning flow to the user
315         // check for success once every 2 secs, up to 20 secs
316         // true connection comes from the handledConnected call back
317         for (int i = 0; i < 10; i++) {
318             if (!this.online) {
319                 try {
320                     Thread.sleep(2000);
321                 } catch (InterruptedException e) {
322                     e.printStackTrace();
323                 }
324             } else {
325                 return;
326             }
327         }
328     }
329 
330 
331     /***
332      * Sign off from aim server
333      */
334     public void signOff() {
335         // cancel the ping until signon is called again
336         watchdogCheck.cancel();
337         watchdogVerify.cancel();
338         signoff("User request");
339     }
340 
341 
342     /***
343      * Main processing method for the AIMClient object
344      */
345     public void run() {
346         int length;
347         seqNo = (int) Math.floor(Math.random() * 65535.0);
348 
349         // AOL likes to have a bunch of bogus IPs for some reason, so lets try
350         // them all until one works
351         InetAddress[] loginIPs = null;
352         try {
353             loginIPs = InetAddress.getAllByName(loginServer);
354         } catch (UnknownHostException e) {
355             signoff("0");
356             generateError("Signon err", e.getMessage());
357             return;
358         }
359 
360         for (int i = 0; i < loginIPs.length; i++) {
361             try {
362                 logger.fine("Attempting to logon using IP:" + loginIPs[i]);
363                 // * Client connects to TOC
364                 connection = new Socket(loginIPs[i], loginPort);
365                 connection.setSoTimeout(10000);
366                 in = new DataInputStream(connection.getInputStream());
367                 out = new DataOutputStream(new BufferedOutputStream(connection.getOutputStream()));
368                 logger.fine("Successfully connected using IP:" + loginIPs[i]);
369                 break;
370             } catch (Exception e) {
371                 // try the next one
372             }
373         }
374 
375         if (connection == null || in == null || out == null) {
376             signoff("1");
377             generateError("Signon err", "Unable to establish connection to logon server.");
378             return;
379         }
380 
381         logger.fine("*** Starting AIM CLIENT (SEQNO:" + seqNo + ") ***");
382         try {
383             // * Client sends "FLAPON\r\n\r\n"
384             out.writeBytes("FLAPON\r\n\r\n");
385             out.flush();
386             // 6 byte header, plus 4 FLAP version (1)
387             byte[] signon = new byte[10];
388             // * TOC sends Client FLAP SIGNON
389             in.readFully(signon);
390             // * Client sends TOC FLAP SIGNON
391             out.writeByte(42);// *
392             out.writeByte(1); // SIGNON TYPE
393             out.writeShort(seqNo); // SEQ NO
394             seqNo = (seqNo + 1) & 65535;
395             out.writeShort(name.length() + 8); // data length = username length
396             // + SIGNON DATA
397             out.writeInt(1); // FLAP VERSION
398             out.writeShort(1); // TLF TAG
399             out.writeShort(name.length()); // username length
400             out.writeBytes(name); // usename
401             out.flush();
402 
403             // * Client sends TOC "toc_signon" message
404             frameSend("toc2_signon " + authorizerServer + " " + authorizerPort + " " + name + " " + imRoast(pass)
405                 + " English " + REVISION + " " + toc2MagicNumber(name, pass) + "\0");
406 
407             // * if login fails TOC drops client's connection
408             // else TOC sends client SIGN_ON reply
409             in.skip(4); // seq num
410             length = in.readShort(); // data length
411             signon = new byte[length];
412             in.readFully(signon); // data
413             if (new String(signon).startsWith("ERROR")) {
414                 fromAIM(signon);
415                 logger.severe("Signon error");
416                 signoff("2");
417                 return;
418             }
419 
420             in.skip(4); // seq num
421             length = in.readShort(); // data length
422             signon = new byte[length];
423             in.readFully(signon); // data
424             // * Client sends TOC toc_init_done message
425             frameSend("toc_init_done\0");
426             online = true;
427             generateConnected();
428             frameSend("toc_set_info \"" + info + "\"\0");
429             logger.fine("Done with AIM logon");
430             connection.setSoTimeout(3000);
431         } catch (InterruptedIOException e) {
432             signoff("2.25");
433         } catch (IOException e) {
434             signoff("3");
435         }
436 
437         byte[] data;
438         while (true) {
439             try {
440                 in.skip(4);
441                 length = in.readShort();
442                 data = new byte[length];
443                 in.readFully(data);
444                 fromAIM(data);
445                 // logger.fine("SEQNO:"+seqNo);
446             } catch (InterruptedIOException e) {
447                 // This is normal; read times out when we dont read anything.
448                 // logger.fine("*** AIM ERROR: " + e + " ***");
449             } catch (IOException e) {
450                 logger.severe("*** AIM ERROR: " + e + " ***");
451                 break;
452             }
453         }
454         signoff("Connection reset.");
455     }
456 
457 
458     /***
459      * @param name2
460      * @param pass2
461      * @return
462      */
463     private static int toc2MagicNumber(String username, String password) {
464         int sn = username.charAt(0) - 96;
465         int pw = password.charAt(0) - 96;
466 
467         int a = sn * 7696 + 738816;
468         int b = sn * 746512;
469         int c = pw * a;
470 
471         return c - a + b + 71665152;
472     }
473 
474 
475     /***
476      * Register a listener to recieve aim events
477      * 
478      * @param listener
479      *            The listener
480      */
481     public void addAIMListener(AIMListener listener) {
482         aimListeners.add(listener);
483     }
484 
485 
486     /***
487      * Send a message to a buddy
488      * 
489      * @param buddy
490      * @param text
491      */
492     public void sendMessage(AIMBuddy buddy, String text) {
493         if ((buddy == null) || buddy.isBanned()) {
494             return;
495         }
496 
497         if (buddy.isOnline()) {
498             sendMesg(buddy.getName(), text);
499         } else {
500             // for some reason we are sending a message to an offline buddy
501             // this will generate a status request for them (this message will
502             // be lost, but if they are online, we should get an update)
503             try {
504                 frameSend("toc_get_status " + imNormalize(buddy.getName()) + "\0");
505             } catch (IOException e) {
506                 logger.severe("Error sending status request for offline buddy: " + e.getMessage());
507             }
508         }
509     }
510 
511 
512     /***
513      * Add a single budy
514      * 
515      * @param buddy
516      *            The buddy to add
517      */
518     public void addBuddy(AIMBuddy buddy) {
519         if (buddy == null) {
520             return;
521         }
522 
523         if (getBuddy(buddy.getName()) != null) {
524             return;
525         }
526 
527         if (this.online) {
528             String toBeSent = "toc2_new_buddies {g:" + buddy.getGroup() + "\nb:" + imNormalize(buddy.getName()) + "\n}";
529             try {
530                 frameSend(toBeSent + "\0");
531             } catch (IOException e) {
532                 logger.severe(e.toString());
533                 signoff("Error adding buddy");
534             }
535         }
536 
537         // logger.fine("Added buddy to hash");
538         buddyHash.put(imNormalize(buddy.getName()), buddy);
539     }
540 
541 
542     /***
543      * Convience method for adding multiple buddies
544      * 
545      * @param buddyList
546      *            List of AIMBuddy
547      */
548     public void addBuddies(List buddyList) {
549         // make a list of buddys for each "group"
550         Map groupMap = createGroupMap(buddyList);
551 
552         // iterate over the groups and send the buddies
553         Iterator groupIter = groupMap.keySet().iterator();
554         while (groupIter.hasNext()) {
555             String group = (String) groupIter.next();
556             String currentlist = "toc2_new_buddies {g:" + group + "\n";
557             List groupList = (List) groupMap.get(group);
558             for (int i = 0; i < groupList.size(); i++) {
559                 AIMBuddy buddy = (AIMBuddy) groupList.get(i);
560                 buddyHash.put(imNormalize(buddy.getName()), buddy);
561                 currentlist += "b:" + imNormalize(buddy.getName()) + "\n";
562                 if (currentlist.length() > 1800) {
563                     try {
564                         frameSend(currentlist + "}\0");
565                         currentlist = "toc2_new_buddies {g:" + group + "\n";
566                     } catch (IOException e) {
567                         e.printStackTrace();
568                         logger.severe("ERROR adding buddies.");
569                     }
570                 }
571             }
572             // send the left overs (if any)
573             if (currentlist.length() > ("toc2_new_buddies {g:" + group + "\n").length()) {
574                 try {
575                     frameSend(currentlist + "}\0");
576                 } catch (IOException e) {
577                     e.printStackTrace();
578                     logger.severe("ERROR adding buddies.");
579                 }
580             }
581 
582         }
583     }
584 
585 
586     /***
587      * Create a Map of List of buddies in the same group
588      * 
589      * @param buddyList
590      *            a list of buddies
591      * @return a Map <String, List> keyed with group name with value a list of
592      *         buddies in that group
593      */
594     private Map createGroupMap(List buddyList) {
595         // <group name,List of buddy>
596         Map groupMap = new HashMap();
597 
598         // iterate the buddies and group them by group name
599         for (Iterator iter = buddyList.iterator(); iter.hasNext();) {
600             Object obj = iter.next();
601             if (obj instanceof AIMBuddy) {
602                 AIMBuddy buddy = (AIMBuddy) obj;
603                 String group = buddy.getGroup();
604                 // pull the list of buddies in this buddy's group
605                 List groupList = (List) groupMap.get(group);
606                 if (groupList == null) {
607                     // first buddy in this group, make a new list
608                     groupList = new ArrayList();
609                     groupMap.put(group, groupList);
610                 }
611                 // add the buddy to the list of buddies in this group
612                 groupList.add(buddy);
613             }
614         }
615         return groupMap;
616     }
617 
618 
619     /***
620      * Remove a single budy
621      * 
622      * @param buddy
623      *            The buddy to add
624      */
625     public void removeBuddy(AIMBuddy buddy) {
626         if (buddy == null) {
627             return;
628         }
629 
630         if (getBuddy(buddy.getName()) == null) {
631             return;
632         }
633 
634         String buddyname = imNormalize(buddy.getName());
635 
636         String toBeSent = "toc2_remove_buddy";
637         try {
638             frameSend(toBeSent + " " + buddyname + " " + buddy.getGroup() + "\0");
639         } catch (IOException e) {
640             logger.severe(e.toString());
641             signoff("Error removing buddy.");
642         }
643 
644         // logger.fine("Removed buddy from hash");
645         buddyHash.remove(imNormalize(buddy.getName()));
646     }
647 
648 
649     /***
650      * Convience method for removing multiple buddies
651      * 
652      * @param buddyList
653      *            List of AIMBuddy
654      */
655     public void removeBuddies(List buddyList) {
656         // make a list of buddys for each "group"
657         Map groupMap = createGroupMap(buddyList);
658 
659         // iterate over the groups and remove the buddies
660         Iterator groupIter = groupMap.keySet().iterator();
661         while (groupIter.hasNext()) {
662             String group = (String) groupIter.next();
663             String currentlist = "toc2_remove_buddy";
664             List groupList = (List) groupMap.get(group);
665             for (int i = 0; i < groupList.size(); i++) {
666                 AIMBuddy buddy = (AIMBuddy) groupList.get(i);
667                 buddyHash.remove(imNormalize(buddy.getName()));
668                 currentlist += " " + imNormalize(buddy.getName());
669                 if (currentlist.length() > 1800) {
670                     try {
671                         frameSend(currentlist + " " + group + "\0");
672                         currentlist = "toc2_remove_buddy";
673                     } catch (IOException e) {
674                         e.printStackTrace();
675                         logger.severe("ERROR removing buddies.");
676                     }
677                 }
678             }
679             // remove the left overs (if any)
680             if (currentlist.length() > "toc2_remove_buddy".length()) {
681                 try {
682                     frameSend(currentlist + " " + group + "\0");
683                 } catch (IOException e) {
684                     e.printStackTrace();
685                     logger.severe("ERROR adding buddies.");
686                 }
687             }
688 
689         }
690     }
691 
692 
693     /***
694      * Warn a buddy
695      * 
696      * @param buddy
697      */
698     public void sendWarning(AIMBuddy buddy) {
699         if (buddy == null) {
700             return;
701         }
702 
703         logger.fine("Attempting to warn: " + buddy.getName() + ".");
704 
705         String work = "toc_evil ";
706         work = work.concat(imNormalize(buddy.getName()));
707         work = work.concat(" norm \0");
708         // logger.fine(work);
709         try {
710             frameSend(work);
711         } catch (IOException e) {
712             signoff("9");
713         }
714     }
715 
716 
717     /***
718      * tell aim to ignore a buddy
719      * 
720      * @param buddy
721      */
722     public void banBuddy(AIMBuddy buddy) {
723         if ((buddy == null) || (buddy.getName().length() == 0)) {
724             return;
725         }
726 
727         if (getBuddy(buddy.getName()) == null) {
728             return;
729         }
730         buddy.setBanned(true);
731         sendDeny(imNormalize(buddy.getName()));
732     }
733 
734 
735     /***
736      * tell aim to ignore a buddy
737      * 
738      * @param buddyname
739      */
740     private void sendDeny(String buddyname) {
741         if (buddyname.length() == 0) {
742             logger.fine("Attempting to permit all.");
743         } else {
744             logger.fine("Attempting to deny: " + buddyname + ".");
745         }
746 
747         String toBeSent = "toc2_add_deny";
748         try {
749             frameSend(toBeSent + " " + buddyname + "\0");
750         } catch (IOException e) {
751             logger.severe(e.toString());
752             signoff("7.75");
753         }
754     }
755 
756 
757     /***
758      * tell aim to permit a buddy
759      * 
760      * @param buddyname
761      */
762     private void sendPermit(String buddyname) {
763         logger.fine("Attempting to permit: " + buddyname + ".");
764 
765         String toBeSent = "toc2_add_permit";
766         try {
767             frameSend(toBeSent + " " + buddyname + "\0");
768         } catch (IOException e) {
769             logger.severe(e.getMessage());
770             signoff("7.875");
771         }
772     }
773 
774 
775     /***
776      * protocol methods *
777      * 
778      * @param toBeSent
779      * @exception IOException
780      *                Description of Exception
781      */
782     private void frameSend(String toBeSent) throws IOException {
783         if (sendLimit < MAX_POINTS) {
784             sendLimit += ((System.currentTimeMillis() - lastFrameSendTime) / RECOVER_RATE);
785             // never let the limit exceed the max, else this code won't work
786             // right
787             sendLimit = Math.min(MAX_POINTS, sendLimit);
788             if (sendLimit < MAX_POINTS) {
789                 // sendLimit could be less than 0, this still works properly
790                 logger.fine("Current send limit=" + sendLimit + " out of " + MAX_POINTS);
791                 try {
792                     // this will wait for every point below the max
793                     int waitAmount = MAX_POINTS - sendLimit;
794                     logger.fine("Delaying send " + waitAmount + " units");
795                     Thread.sleep(RECOVER_RATE * waitAmount);
796                     sendLimit += waitAmount;
797                 } catch (InterruptedException ie) {
798                 }
799             }
800         }
801         out.writeByte(42); // *
802         out.writeByte(2); // DATA
803         out.writeShort(seqNo); // SEQ NO
804         seqNo = (seqNo + 1) & 65535;
805         out.writeShort(toBeSent.length()); // DATA SIZE
806         out.writeBytes(toBeSent); // DATA
807         out.flush();
808 
809         // sending is more expensive the higher our warning level
810         // this should decrement between 1 and 10 points (exponentially)
811         int warnAmount = getBuddy(this.name).getWarningAmount();
812         sendLimit -= (1 + Math.pow((3 * warnAmount) / 100, 2));
813         lastFrameSendTime = System.currentTimeMillis();
814     }
815 
816 
817     /***
818      * Send message event to all listeners.
819      * 
820      * @param from
821      * @param request
822      */
823     private void generateMessage(String from, String request) {
824         AIMBuddy aimbud = getBuddy(from);
825         if (aimbud == null) {
826             if (autoAddUsers) {
827                 aimbud = new AIMBuddy(from);
828                 addBuddy(aimbud);
829                 aimbud.setOnline(true);
830             } else {
831                 logger.info("MESSAGE FROM A NON BUDDY(" + from + ")");
832                 // only send a response if a non-empty one is configured
833                 if ((nonUserResponse != null) && !nonUserResponse.equals("")) {
834                     sendMesg(from, nonUserResponse);
835                 }
836                 return;
837             }
838         }
839 
840         if (aimbud.isBanned()) {
841             logger.fine("Ignoring message from banned user (" + from + "):" + request);
842         } else {
843             for (int i = 0; i < aimListeners.size(); i++) {
844                 try {
845                     ((AIMListener) aimListeners.get(i)).handleMessage(aimbud, request);
846                 } catch (Exception e) {
847                     e.printStackTrace();
848                 }
849             }
850         }
851     }
852 
853 
854     /***
855      * Send warning event to all listeners.
856      * 
857      * @param from
858      * @param amount
859      *            of warning
860      */
861     private void generateWarning(String from, int amount) {
862         AIMBuddy aimbud = getBuddy(from);
863         for (int i = 0; i < aimListeners.size(); i++) {
864             try {
865                 ((AIMListener) aimListeners.get(i)).handleWarning(aimbud, amount);
866             } catch (Exception e) {
867                 e.printStackTrace();
868             }
869         }
870     }
871 
872 
873     /***
874      * Send connected event to all listeners.
875      */
876     private void generateConnected() {
877         for (int i = 0; i < aimListeners.size(); i++) {
878             try {
879                 ((AIMListener) aimListeners.get(i)).handleConnected();
880             } catch (Exception e) {
881                 e.printStackTrace();
882             }
883         }
884     }
885 
886 
887     /***
888      * Send disconnected event to all listeners.
889      */
890     private void generateDisconnected() {
891         for (int i = 0; i < aimListeners.size(); i++) {
892             try {
893                 ((AIMListener) aimListeners.get(i)).handleDisconnected();
894             } catch (Exception e) {
895                 e.printStackTrace();
896             }
897         }
898     }
899 
900 
901     /***
902      * Send error event to all listeners.
903      * 
904      * @param error
905      *            code
906      * @param message
907      */
908     private void generateError(String error, String message) {
909         for (int i = 0; i < aimListeners.size(); i++) {
910             try {
911                 ((AIMListener) aimListeners.get(i)).handleError(error, message);
912             } catch (Exception e) {
913                 e.printStackTrace();
914             }
915         }
916     }
917 
918 
919     /***
920      * Send buddy sign on event to all listeners.
921      * 
922      * @param buddy
923      *            that signed on
924      * @param message
925      */
926     private void generateBuddySignOn(String buddy, String message) {
927         AIMBuddy aimbud = getBuddy(buddy);
928         if (aimbud == null) {
929             logger.severe("ERROR:  NOTIFICATION ABOUT NON BUDDY(" + buddy + ")");
930             return;
931         }
932 
933         if (!aimbud.isOnline()) {
934             aimbud.setOnline(true);
935             for (int i = 0; i < aimListeners.size(); i++) {
936                 try {
937                     ((AIMListener) aimListeners.get(i)).handleBuddySignOn(aimbud, message);
938                 } catch (Exception e) {
939                     e.printStackTrace();
940                 }
941             }
942         }
943     }
944 
945 
946     /***
947      * Send buddy sign off event to all listeners.
948      * 
949      * @param buddy
950      *            that signed off
951      * @param message
952      */
953     private void generateBuddySignOff(String buddy, String message) {
954         AIMBuddy aimbud = getBuddy(buddy);
955         if (aimbud == null) {
956             logger.severe("ERROR:  NOTIFICATION ABOUT NON BUDDY(" + buddy + ")");
957             return;
958         }
959 
960         // logger.fine("XML = \n" + aimbud.toXML());
961         aimbud.setOnline(false);
962         for (int i = 0; i < aimListeners.size(); i++) {
963             try {
964                 ((AIMListener) aimListeners.get(i)).handleBuddySignOff(aimbud, message);
965             } catch (Exception e) {
966                 e.printStackTrace();
967             }
968         }
969     }
970 
971 
972     /***
973      * Send buddy is available event to all listeners.
974      * 
975      * @param buddy
976      *            The subject of the change.
977      * @param message
978      *            DOCUMENT ME!
979      */
980     private void generateBuddyAvailable(String buddy, String message) {
981         AIMBuddy aimbud = getBuddy(buddy);
982         if (aimbud == null) {
983             logger.severe("ERROR:  NOTIFICATION ABOUT NON BUDDY(" + buddy + ")");
984             return;
985         }
986         for (int i = 0; i < aimListeners.size(); i++) {
987             try {
988                 ((AIMListener) aimListeners.get(i)).handleBuddyAvailable(aimbud, message);
989             } catch (Exception e) {
990                 e.printStackTrace();
991             }
992         }
993     }
994 
995 
996     /***
997      * Send buddy is unavailable event to all listeners.
998      * 
999      * @param buddy
1000      *            The subject of the change.
1001      * @param message
1002      *            DOCUMENT ME!
1003      */
1004     private void generateBuddyUnavailable(String buddy, String message) {
1005         AIMBuddy aimbud = getBuddy(buddy);
1006         if (aimbud == null) {
1007             logger.severe("ERROR:  NOTIFICATION ABOUT NON BUDDY(" + buddy + ")");
1008             return;
1009         }
1010 
1011         for (int i = 0; i < aimListeners.size(); i++) {
1012             try {
1013                 ((AIMListener) aimListeners.get(i)).handleBuddyUnavailable(aimbud, message);
1014             } catch (Exception e) {
1015                 e.printStackTrace();
1016             }
1017         }
1018     }
1019 
1020 
1021     /***
1022      * message recieved from aim
1023      * 
1024      * @param buffer
1025      */
1026     private void fromAIM(byte[] buffer) {
1027         try {
1028             String inString = new String(buffer);
1029 
1030             logger.fine("*** AIM: " + inString + " ***");
1031             StringTokenizer inToken = new StringTokenizer(inString, ":");
1032             String command = inToken.nextToken();
1033             if (command.equals("IM_IN2")) {
1034                 // treat every message received as verification
1035                 this.connectionVerified = true;
1036 
1037                 String from = imNormalize(inToken.nextToken());
1038                 // whats this?
1039                 inToken.nextToken();
1040                 // whats this?
1041                 inToken.nextToken();
1042                 String mesg = inToken.nextToken();
1043                 while (inToken.hasMoreTokens()) {
1044                     mesg = mesg + ":" + inToken.nextToken();
1045                 }
1046 
1047                 String request = stripHTML(mesg);
1048 
1049                 if ((from.equalsIgnoreCase(this.name)) && (request.equals(AIMClient.PING))) {
1050                     logger.fine("AIM CONNECTION VERIFIED(" + new Date() + ").");
1051                     return;
1052                 }
1053 
1054                 logger.fine("*** AIM MESSAGE: " + from + " > " + request + " ***");
1055 
1056                 // CALL ALL LISTENERS HERE
1057                 generateMessage(from, request.trim());
1058                 return;
1059             }
1060 
1061             if (command.equals("CONFIG2")) {
1062                 if (inToken.hasMoreElements()) {
1063                     String config = inToken.nextToken();
1064                     while (inToken.hasMoreTokens()) {
1065                         config = config + ":" + inToken.nextToken();
1066                     }
1067                     processConfig(config);
1068                     logger.fine("*** AIM CONFIG RECEIVED ***");
1069                 } else {
1070                     setPermitMode(PERMIT_ALL);
1071                     logger.fine("*** AIM NO CONFIG RECEIVED ***");
1072                 }
1073                 return;
1074             }
1075 
1076             if (command.equals("EVILED")) {
1077                 int amount = Integer.parseInt(inToken.nextToken());
1078                 String from = "anonymous";
1079                 if (inToken.hasMoreElements()) {
1080                     from = imNormalize(inToken.nextToken());
1081                 }
1082 
1083                 // if what we have is less than what the server just sent, its
1084                 // a warning
1085                 // otherwise it was just a server decrement update
1086                 if (getBuddy(name).getWarningAmount() < amount) {
1087                     generateWarning(from, amount);
1088                 }
1089 
1090                 return;
1091             }
1092 
1093             if (command.equals("UPDATE_BUDDY2")) {
1094                 String bname = imNormalize(inToken.nextToken());
1095                 AIMBuddy aimbud = getBuddy(bname);
1096                 if (aimbud == null) {
1097                     logger.severe("ERROR:  NOTIFICATION ABOUT NON BUDDY(" + bname + ")");
1098                     return;
1099                 }
1100                 String stat = inToken.nextToken();
1101                 if (stat.equals("T")) {
1102                     generateBuddySignOn(bname, "INFO");
1103                     // logger.fine("Buddy:" + name + " just signed on.");
1104                 } else if (stat.equals("F")) {
1105                     generateBuddySignOff(bname, "INFO");
1106                     // logger.fine("Buddy:" + name + " just signed off.");
1107                 }
1108                 int evilAmount = Integer.parseInt(inToken.nextToken());
1109                 aimbud.setWarningAmount(evilAmount);
1110                 if (stat.equals("T")) { // See whether user is available.
1111                     String signOnTime = inToken.nextToken();
1112 
1113                     // TODO: what is the format of this?
1114                     // System.err.println(bname+" signon="+signOnTime);
1115                     String idleTime = inToken.nextToken();
1116                     // System.err.println(bname+"
1117                     // idle="+Integer.valueOf(idleTime).intValue()+" mins");
1118                     if (-1 != inToken.nextToken().indexOf('U')) {
1119                         generateBuddyUnavailable(bname, "INFO");
1120                     } else {
1121                         generateBuddyAvailable(bname, "INFO");
1122                     }
1123                 }
1124 
1125                 return;
1126             }
1127 
1128             if (command.equals("ERROR")) {
1129                 String error = inToken.nextToken();
1130                 logger.severe("*** AIM ERROR: " + error + " ***");
1131                 if (error.equals("901")) {
1132                     generateError(error, "Not currently available");
1133                     // logger.fine("Not currently available");
1134                     return;
1135                 }
1136 
1137                 if (error.equals("902")) {
1138                     generateError(error, "Warning not currently available");
1139                     // logger.fine("Warning not currently available");
1140                     return;
1141                 }
1142 
1143                 if (error.equals("903")) {
1144                     generateError(error, "Message dropped, sending too fast");
1145                     // logger.fine("Message dropped, sending too fast");
1146                     return;
1147                 }
1148 
1149                 if (error.equals("960")) {
1150                     String person = inToken.nextToken();
1151                     generateError(error, "Sending messages too fast to " + person);
1152                     // logger.fine("Sending messages too fast to " + person);
1153                     return;
1154                 }
1155 
1156                 if (error.equals("961")) {
1157                     String person = inToken.nextToken();
1158                     generateError(error, person + " sent you too big a message");
1159                     // logger.fine(person + " sent you too big a message");
1160                     return;
1161                 }
1162 
1163                 if (error.equals("962")) {
1164                     String person = inToken.nextToken();
1165                     generateError(error, person + " sent you a message too fast");
1166                     // logger.fine(person + " sent you a message too fast");
1167                     return;
1168                 }
1169 
1170                 if (error.equals("Signon err")) {
1171                     String text = inToken.nextToken();
1172                     generateError(error, "AIM Signon failure: " + text);
1173 
1174                     // logger.fine("AIM Signon failure: " + text);
1175                     signoff("5");
1176                 }
1177 
1178                 return;
1179             }
1180         } catch (Exception e) {
1181             logger.severe("ERROR: failed to handle aim protocol properly");
1182             e.printStackTrace();
1183         }
1184     }
1185 
1186 
1187     /***
1188      * Processes AIM server-passed config string
1189      * 
1190      * @param config
1191      *            A properly formated TOC configuration.
1192      */
1193     private void processConfig(String config) {
1194         int new_permit_mode = PERMIT_ALL;
1195         BufferedReader br = new BufferedReader(new StringReader(config));
1196         try {
1197             String current_group = DEFAULT_GROUP;
1198             String line;
1199             while (null != (line = br.readLine())) {
1200                 if (line.equals("done")) {
1201                     break;
1202                 }
1203                 char type = line.charAt(0);
1204                 String arg = line.substring(2);
1205                 switch (type) {
1206                     case 'g' :
1207                         current_group = arg;
1208                         break;
1209                     case 'b' :
1210                         // make a new buddy if they dont exist locally
1211                         AIMBuddy buddy = null;
1212                         buddy = (AIMBuddy) buddyHash.get(imNormalize(arg));
1213                         if (buddy == null) {
1214                             buddy = new AIMBuddy(arg, current_group);
1215                             buddyHash.put(imNormalize(arg), buddy);
1216                         } else {
1217                             // they already exist, so just take the server's
1218                             // word
1219                             // for the group they belong in
1220                             buddy.setGroup(current_group);
1221                         }
1222                         break;
1223                     case 'p' :
1224                         permitted.add(imNormalize(arg));
1225                         break;
1226                     case 'd' :
1227                         denied.add(imNormalize(arg));
1228                         break;
1229                     case 'm' :
1230                         new_permit_mode = Integer.parseInt(arg);
1231                         break;
1232                 }
1233             }
1234         } catch (IOException e) {
1235             logger.warning("Error reading configuration.");
1236             signoff("2.25");
1237             return;
1238         }
1239 
1240         // this will "readd" existing buddies, but thats ok
1241         addBuddies(new ArrayList(buddyHash.values()));
1242         setPermitMode(new_permit_mode);
1243     }
1244 
1245 
1246     /***
1247      * internal method to send message to aim
1248      * 
1249      * @param to
1250      * @param text
1251      *            to send
1252      */
1253     void sendMesg(String to, String text) {
1254         if (text.length() >= 1024) {
1255             text = text.substring(0, 1024);
1256         }
1257         logger.fine("Sending Message " + to + " > " + text);
1258 
1259         String work = "toc2_send_im ";
1260         work = work.concat(to);
1261         work = work.concat(" \"");
1262         for (int i = 0; i < text.length(); i++) {
1263             switch (text.charAt(i)) {
1264                 case '$' :
1265                 case '{' :
1266                 case '}' :
1267                 case '[' :
1268                 case ']' :
1269                 case '(' :
1270                 case ')' :
1271                 case '\"' :
1272                 case '//' :
1273                     work = work.concat("//" + text.charAt(i));
1274                     break;
1275                 default :
1276                     work = work.concat("" + text.charAt(i));
1277                     break;
1278             }
1279         }
1280 
1281         work = work.concat("\"\0");
1282         // logger.fine(work);
1283         try {
1284             frameSend(work);
1285         } catch (IOException e) {
1286             logger.severe("*** AIM ERROR: sending message.");
1287             e.printStackTrace();
1288             signoff("9");
1289         }
1290     }
1291 
1292 
1293     /***
1294      * Change availability. If the reason is the empty String, the user will be
1295      * made avaiable. Otherwise, it will be made away.
1296      * 
1297      * @param reason
1298      *            The reason explaining why the user is not avaiable.
1299      */
1300     private void sendAway(String reason) {
1301         final String work = "toc_set_away \"" + reason + "\"\0";
1302 
1303         try {
1304             frameSend(work);
1305         } catch (IOException e) {
1306             signoff("10");
1307         }
1308     }
1309 
1310 
1311     /***
1312      * sign off
1313      * 
1314      * @param place
1315      */
1316     private void signoff(String place) {
1317         online = false;
1318         logger.fine("Trying to close IM (" + place + ").....");
1319         try {
1320             if (null != out) {
1321                 out.close();
1322             }
1323             if (null != in) {
1324                 in.close();
1325             }
1326             if (null != connection) {
1327                 connection.close();
1328             }
1329         } catch (IOException e) {
1330             logger.severe(e.toString());
1331         }
1332 
1333         generateDisconnected();
1334         logger.fine("*** AIM CLIENT SIGNED OFF.");
1335     }
1336 
1337 
1338     /***
1339      * Add a buddy to the denied list
1340      * 
1341      * @param buddy
1342      */
1343     public void denyBuddy(AIMBuddy buddy) {
1344         String bname = imNormalize(buddy.getName());
1345         permitted.remove(bname);
1346         denied.add(bname);
1347         sendDeny(bname);
1348     }
1349 
1350 
1351     /***
1352      * Add a buddy to the permitted list
1353      * 
1354      * @param buddy
1355      */
1356     public void permitBuddy(AIMBuddy buddy) {
1357         String bname = imNormalize(buddy.getName());
1358         denied.remove(bname);
1359         permitted.add(bname);
1360         sendPermit(bname);
1361     }
1362 
1363 
1364     /***
1365      * Gets the permit mode that is set on the server.
1366      * 
1367      * @return int representation (see public statics) of current permit mode.
1368      */
1369     public int getPermitMode() {
1370         return permitMode;
1371     }
1372 
1373 
1374     /***
1375      * Sets the permit mode on the server. (Use constants from AIMSender)
1376      * 
1377      * @param mode
1378      */
1379     public void setPermitMode(int mode) {
1380         if (mode < 1 || mode > 5) {
1381             logger.info("Invalid permit mode, ignoring:" + mode);
1382             return;
1383         } else if (mode == DENY_SOME && this.denied.size() == 0) {
1384             logger.info("Attempting to deny some, and none are denied, ignoring.");
1385             return;
1386         } else if (mode == PERMIT_SOME && this.permitted.size() == 0) {
1387             logger.info("Attempting to permit some, and none are permitted, ignoring.");
1388             return;
1389         }
1390 
1391         logger.info("Setting permit mode to:" + mode);
1392         permitMode = mode;
1393         try {
1394             frameSend("toc2_set_pdmode " + permitMode + "\0");
1395         } catch (IOException e) {
1396             e.printStackTrace();
1397             logger.severe("ERROR setting permit mode!");
1398         }
1399     }
1400 
1401 
1402     /***
1403      * Clear unvailable message
1404      */
1405     public void setAvailable() {
1406         sendAway("");
1407     }
1408 
1409 
1410     /***
1411      * Set unvailable message
1412      * 
1413      * @param reason
1414      */
1415     public void setUnavailable(String reason) {
1416         sendAway(reason);
1417     }
1418 
1419 
1420     /***
1421      * Try to verify our connections by messaging ourselves. Takes 2 instances
1422      * to check. One to send the message (sender=true), One to check the result
1423      * (sender=false)
1424      * 
1425      * @author Scott Oster
1426      * @created February 28, 2002
1427      */
1428     static class AimConnectionCheck extends TimerTask {
1429         AIMClient aim;
1430         private boolean sender;
1431 
1432 
1433         /***
1434          * Constructor for the AimConnectionCheck object
1435          * 
1436          * @param aim
1437          *            handle to aim
1438          * @param sender
1439          *            which instance it is
1440          */
1441         public AimConnectionCheck(AIMClient aim, boolean sender) {
1442             this.aim = aim;
1443             this.sender = sender;
1444         }
1445 
1446 
1447         /***
1448          * Main processing method for the AimConnectionCheck object
1449          */
1450         public void run() {
1451             try {
1452                 if (sender) {
1453                     aim.connectionVerified = false;
1454                     // only message if we are online (when we get reconnected
1455                     // online will be true)
1456                     if (aim.online) {
1457                         aim.sendMesg(aim.name, AIMClient.PING);
1458                     }
1459                 } else {
1460                     // need to see if we got a response
1461                     if (!aim.connectionVerified) {
1462                         // restart the connection if we didnt see the message
1463                         logger.info("*** AIM -- CONNECTION PROBLEM(" + new Date() + "): Connection was not verified!");
1464                         logger.info("****** Assuming it was dropped, issuing restart.");
1465                         aim.signoff("Connection Dropped!");
1466                         new Thread(aim).start();
1467                     }
1468                 }
1469             } catch (Exception e) {
1470                 e.printStackTrace();
1471             }
1472         }
1473     }
1474 }