<commit_before><?php
/* This class is part of the XP framework
 *
 * $Id$ 
 */

  uses(
    'peer.Socket', 
    'peer.irc.IRCConstants',
    'peer.irc.IRCUser',
    'peer.irc.IRCConnectionListener'
  );

  /**
   * IRC Connection
   *
   * Usage example:
   * <code>
   *   uses(
   *     'peer.irc.IRCConnection', 
   *     'util.log.Logger',
   *     'util.log.FileAppender',
   *     'KrokerdilBotListener'
   *   );
   *   
   *   $c= &new IRCConnection(new IRCUser('KrokerdilBot'), 'irc.xxx.net');
   *   
   *   $l= &Logger::getInstance();
   *   $cat= &$l->getCategory();
   *   $cat->addAppender(new FileAppender('php://stderr'));
   *   $c->setTrace($cat);
   *   
   *   $c->addListener(new KrokerdilBotListener());
   *   try(); {
   *     $c->open();
   *     $c->run();
   *     $c->close();
   *   } if (catch('Exception', $e)) {
   *     $e->printStackTrace();
   *   }
   * </code>
   *
   * @see      xp://peer.irc.IRCConstants
   * @see      xp://peer.irc.IRCUser
   * @purpose  A socket connection to a RFC-1459 compatible IRC server.
   */
  class IRCConnection extends Object {
    var
      $sock         = NULL,
      $cat          = NULL,
      $user         = NULL,
      $listeners    = array();
      
    /**
     * Constructor
     *
     * @access  public
     * @param   &peer.irc.IRCUser user
     * @param   string server
     * @param   int port default 6667
     */
    function __construct(&$user, $server, $port= 6667) {
      
      $this->user= &$user;
      $this->sock= &new Socket($server, $port);
    }
    
    /**
     * Adds a listener
     *
     * @access  public
     * @see     xp://peer.irc.IRCConnectionListener
     * @param   &peer.irc.IRCConnectionListener listener
     * @return  &peer.irc.IRCConnectionListener the listener added
     */
    function &addListener(&$listener) {
      $this->listeners[]= &$listener;
      return $listener;
    }
    
    /**
     * Set a logger category for debugging
     *
     * @access  public
     * @param   &util.log.LogCategory cat
     */
    function setTrace(&$cat) {
      $this->cat= &$cat;
    }
  
    /**
     * Opens connection to the IRC server. Also sets the socket to 
     * blocking mode.
     *
     * @access  public
     * @see     xp://peer.Socket#connect
     * @see     xp://peer.Socket#setBlocking
     * @throws  peer.SocketException in case connecting fails
     */
    function open() {
      if (!$this->sock->connect()) return FALSE;
      $this->sock->setBlocking(TRUE);
      $this->notify('connect', $this->sock->host, $this->sock->port); 
    }
    
    /**
     * Notify all listeners
     *
     * @access  protected
     * @param   string event
     * @param   mixed* parameters
     */
    function notify() {
      $a= func_get_args();
      $function= 'on'.ucfirst(array_shift($a));
      array_unshift($a, $this);
      for ($i= 0, $s= sizeof($this->listeners); $i < $s; $i++) {
        $this->cat && $this->cat->debugf(
          'Calling %s::%s(<self>, %s)', 
          $this->listeners[$i]->getClassName(), 
          $function, 
          var_export(array_slice($a, 1), 1)
        );
        call_user_func_array(array(&$this->listeners[$i], $function), $a);
      }
    }
    
    /**
     * Process a single line of read data
     *
     * @access  public
     * @param   string r the data
     * @return  bool TRUE when line was successfully processed
     */
    function process($r) { 
      if (':' == $r{0}) {
      
        if (4 == sscanf($r, ":%s %d %s %[^\r]", $server, $code, $target, $data)) {

          // Server messages
          // :irc.xxx.net 372 KrokerdilBot :- Excuse of the hour:
          // :irc.xxx.net 353 KrokerdilBot = #test :KrokerdilBot @krokerdil
          // :irc.xxx.net 366 KrokerdilBot #test :End of /NAMES list.
          // :irc.xxx.net 376 KrokerdilBot :End of /MOTD command.
          $this->cat && $this->cat->debugf('    Server message #%d %s', $code, IRCConstants::nameOf($code));
          
          switch ($code) {
            case IRC_REPLY_MOTDSTART:
            case IRC_REPLY_MOTD:
            case IRC_REPLY_ENDOFMOTD:
              $this->notify(substr(IRCConstants::nameOf($code), 6), $server, $target, $data);
              break;
            
            default:
              $this->notify('serverMessage', $server, $code, $target, $data);
          }
        } elseif (4 <= sscanf($r, ":%[^!]!%s %s %s %[^\r]", $nick, $user, $action, $target, $data)) {

          // Other messages
          switch (strtolower($action)) {

            // :krokerdil!<EMAIL> PRIVMSG KrokerdilBot :Wop
            // :krokerdil!<EMAIL> PRIVMSG #test :Poff
            // :krokerdil!<EMAIL> PRIVMSG #test :\001ACTION thinks Bot is stoopid\001
            // :krokerdil!<EMAIL> PRIVMSG Bot :\001USERINFO\001
            // :krokerdil!<EMAIL> PRIVMSG KrokerdilBot :Hello\0033Hello
            case 'privmsg':
              if ("\1" == $data{1}) {
                list($command, $params)= explode(' ', substr($data, 2, -1), 2);
                $this->notify($command, $nick, $target, $params);
              } else {
                $this->notify('privateMessage', $nick, $target, substr($data, 1));
              }
              break;

            // :krokerdil!<EMAIL> JOIN :#test
            case 'join':
              $this->notify('joins', substr($target, 1), $nick);
              break;

            // :krokerdil!<EMAIL> PART #test :Leaving
            case 'part':
              $this->notify('parts', $target, $nick, substr($data, 1));
              break;

            // :krokerdil!<EMAIL> MODE #test +v KrokerdilBot
            // :krokerdil!<EMAIL> MODE #test +k HALLO
            // :krokerdil!<EMAIL> MODE #test -k HALLO
            // :krokerdil!<EMAIL> MODE #test +b *!*<EMAIL>@*.xxx.de
            // :krokerdil!<EMAIL> MODE #test +o KrokerdilBot 
            case 'mode':
              list($mode, $params)= explode(' ', $data, 2);
              $this->notify('modeChanges', $nick, $target, $mode, $params);
              break;

            // :krokerdil!<EMAIL> KICK #schlund Bot :IHATEYOU
            case 'kick':
              list($who, $reason)= explode(' ', $data, 2);
              $this->notify('kicks', $target, $nick, $who, substr($reason, 1));
              break;

            // :krokerdil!<EMAIL> QUIT :signoff: Ave
            case 'quit':
              $this->notify('quits', $target, $nick, $data);
              break;            

            // :krokerdil!<EMAIL> NICK :schnutepanzerdil
            case 'nick':
              $this->notify('nickChanges', $nick, substr($target, 1));
              break;
            
            // :krokerdil!<EMAIL> INVITE KrokerdilBot :#test
            case 'invite':
              $this->notify('invite', $nick, $target, substr($data, 1));
              break;
            
            // :krokerdil!<EMAIL> NOTICE #test :8tung baby
            // :krokerdil!<EMAIL> NOTICE KrokerdilBot :8tung baby
            case 'notice':
              $this->notify('notice', $nick, $target, substr($data, 1));
              break;
              
            // :krokerdil!<EMAIL> TOPIC #test :Korkerdl             
            case 'topic':
              $this->notify('topic', $nick, $target, substr($data, 1));
              break;

            default:
              $this->cat && $this->cat->warn('Unrecognized action', $action, '(', $nick, $user, $action, $target, $data, ')');
              return FALSE;
          }
        } else {
          $this->cat && $this->cat->error('Could not parse', $r);
          return FALSE;
        }
      } else {
        $this->cat && $this->cat->error('Malformed message', $r);
        return FALSE;
      }
      return TRUE;
    }
    
    /**
     * Set nickname and update this user's nickname.
     *
     * @access  public
     * @param   string nick
     * @return  bool success TRUE in case the command could be sent
     * @throws  lang.IllegalStateException in case the socket is not connected
     */
    function setNick($nick) {
      if (!$this->sock->isConnected()) {
        return throw(new IllegalStateException('Not connected'));
      }
      if (!$this->writeln('NICK %s', $nick)) return FALSE;
      $this->user->setNick($nick);
      return TRUE;
    }
    
    /**
     * Join a channel
     *
     * @access  public
     * @param   string channel without leading #
     * @param   string keyword default NULL
     * @return  bool success TRUE in case the command could be sent
     * @throws  lang.IllegalStateException in case the socket is not connected
     */
    function join($channel, $keyword= NULL) {
      if (!$this->sock->isConnected()) {
        return throw(new IllegalStateException('Not connected'));
      }
      
      // Be tolerant about channel parameter and rip off leading # if necessary
      if ('#' == $channel{0}) $channel= substr($channel, 1);
      return $this->writeln('JOIN #%s %s', $channel, $keyword);
    }
    
    /**
     * Wrapper around writeln() that sends a PRIVMSG
     *
     * @access  public
     * @param   string dest either a nick or a channel (beginning with "#")
     * @param   string format
     * @param   mixed* format_arguments
     * @return  bool success TRUE in case the command could be sent
     */
    function sendMessage() {
      $dest= array_shift($a= func_get_args());
      return $this->writeln('PRIVMSG %s :%s', $dest, vsprintf(array_shift($a), $a));
    }

    /**
     * Wrapper around writeln() that sends a PRIVMSG with an ACTION
     *
     * @access  public
     * @param   string dest either a nick or a channel (beginning with "#")
     * @param   string format
     * @param   mixed* format_arguments
     * @return  bool success TRUE in case the command could be sent
     */
    function sendAction() {
      $dest= array_shift($a= func_get_args());
      return $this->writeln("PRIVMSG %s :\1ACTION %s\1", $dest, vsprintf(array_shift($a), $a));
    }
    
    /**
     * The main loop in which the socket is polled for data, notifying
     * the attached listeners of events.
     *
     * @access  public
     * @throws  lang.IllegalStateException in case the socket is not connected
     * @throws  io.IOException in case registration failed
     */
    function run() {
      if (!$this->sock->isConnected()) {
        return throw(new IllegalStateException('Not connected'));
      }
      
      // Register with the server
      try(); {
        $this->writeln(
          'USER %s %s %s :%s', 
          $this->user->getUsername(),
          $this->user->getHostname(),
          $this->sock->host,
          $this->user->getRealname()
        );

        // Set nickname
        $this->setNick($this->user->getNick());
      } if (catch('IOException', $e)) {
        return throw($e);
      }
      
      // Loop while socket is not disconnected
      $messages= array();
      while (!$this->sock->eof()) {
        if (!$r= $this->readln()) continue;
        
        // Reply to PINGs immediately
        if ('PING :' == substr($r, 0, 6)) {
          $this->writeln('PONG %s', substr($r, 6));
          $this->notify('ping', substr($r, 6));
          continue;
        }

        // Process data
        $this->process($r);
      }
    }
    
    /**
     * Close the communication socket. Issues a QUIT before 
     *
     * @access  public
     * @see     xp://peer.Socket#close
     * @return  bool success
     */
    function close() {
      if ($this->sock->isConnected()) return TRUE;
      
      $this->notify('disconnect', $this->sock->host, $this->sock->port); 
      
      // Politely say goodbye
      $this->writeln('QUIT');
      return $this->sock->close();
    }

    /**
     * Directly read a line off the socket. The returned line has any 
     * trailing CRLF trimmed off.
     *
     * @access  public
     * @see     xp://peer.Socket#readLine
     * @return  string or FALSE to indicate failure
     */    
    function readln() {
      xp::gc();
      try(); {
        $r= $this->sock->readLine(0x2000);
      } if (catch('IOException', $e)) {
        $this->cat && $this->cat->warn($e);
        return FALSE;
      }
      $this->cat && $this->cat->infof(
        '<<< %s %3d: %s', 
        gettype($r), 
        strlen($r), 
        addcslashes($r, "\0..\37")
      );
      
      return $r;
    }
    
    /**
     * Directly write a string to the socket. A CRLF is added automatically 
     * at the string's end.
     *
     * Note: It seems as if 448 bytes is the maximum length of a single
     * string that can be written e.g. as a PRIVMSG (including the PRIVMSG
     * command and destination). This limit is not checked upon.
     *
     * @access  public
     * @see     xp://peer.Socket#write
     * @param   string format
     * @param   mixed* formatargs
     * @return  bool success
     */
    function writeln() {
      $cmd= vsprintf(array_shift($a= func_get_args()), $a);
      $this->cat && $this->cat->infof(
        '>>> %s %3d: %s', 
        gettype($cmd), 
        strlen($cmd), 
        addcslashes($cmd, "\0..\37")
      );
      
      return $this->sock->write($cmd."\r\n");
    }
  } implements(__FILE__, 'util.log.Traceable');
?>
<commit_msg>- QA: WS fix<commit_after><?php
/* This class is part of the XP framework
 *
 * $Id$ 
 */

  uses(
    'peer.Socket', 
    'peer.irc.IRCConstants',
    'peer.irc.IRCUser',
    'peer.irc.IRCConnectionListener'
  );

  /**
   * IRC Connection
   *
   * Usage example:
   * <code>
   *   uses(
   *     'peer.irc.IRCConnection', 
   *     'util.log.Logger',
   *     'util.log.FileAppender',
   *     'KrokerdilBotListener'
   *   );
   *   
   *   $c= &new IRCConnection(new IRCUser('KrokerdilBot'), 'irc.xxx.net');
   *   
   *   $l= &Logger::getInstance();
   *   $cat= &$l->getCategory();
   *   $cat->addAppender(new FileAppender('php://stderr'));
   *   $c->setTrace($cat);
   *   
   *   $c->addListener(new KrokerdilBotListener());
   *   try(); {
   *     $c->open();
   *     $c->run();
   *     $c->close();
   *   } if (catch('Exception', $e)) {
   *     $e->printStackTrace();
   *   }
   * </code>
   *
   * @see      xp://peer.irc.IRCConstants
   * @see      xp://peer.irc.IRCUser
   * @purpose  A socket connection to a RFC-1459 compatible IRC server.
   */
  class IRCConnection extends Object {
    var
      $sock         = NULL,
      $cat          = NULL,
      $user         = NULL,
      $listeners    = array();
      
    /**
     * Constructor
     *
     * @access  public
     * @param   &peer.irc.IRCUser user
     * @param   string server
     * @param   int port default 6667
     */
    function __construct(&$user, $server, $port= 6667) {
      $this->user= &$user;
      $this->sock= &new Socket($server, $port);
    }
    
    /**
     * Adds a listener
     *
     * @access  public
     * @see     xp://peer.irc.IRCConnectionListener
     * @param   &peer.irc.IRCConnectionListener listener
     * @return  &peer.irc.IRCConnectionListener the listener added
     */
    function &addListener(&$listener) {
      $this->listeners[]= &$listener;
      return $listener;
    }
    
    /**
     * Set a logger category for debugging
     *
     * @access  public
     * @param   &util.log.LogCategory cat
     */
    function setTrace(&$cat) {
      $this->cat= &$cat;
    }
  
    /**
     * Opens connection to the IRC server. Also sets the socket to 
     * blocking mode.
     *
     * @access  public
     * @see     xp://peer.Socket#connect
     * @see     xp://peer.Socket#setBlocking
     * @throws  peer.SocketException in case connecting fails
     */
    function open() {
      if (!$this->sock->connect()) return FALSE;
      $this->sock->setBlocking(TRUE);
      $this->notify('connect', $this->sock->host, $this->sock->port); 
    }
    
    /**
     * Notify all listeners
     *
     * @access  protected
     * @param   string event
     * @param   mixed* parameters
     */
    function notify() {
      $a= func_get_args();
      $function= 'on'.ucfirst(array_shift($a));
      array_unshift($a, $this);
      for ($i= 0, $s= sizeof($this->listeners); $i < $s; $i++) {
        $this->cat && $this->cat->debugf(
          'Calling %s::%s(<self>, %s)', 
          $this->listeners[$i]->getClassName(), 
          $function, 
          var_export(array_slice($a, 1), 1)
        );
        call_user_func_array(array(&$this->listeners[$i], $function), $a);
      }
    }
    
    /**
     * Process a single line of read data
     *
     * @access  public
     * @param   string r the data
     * @return  bool TRUE when line was successfully processed
     */
    function process($r) { 
      if (':' == $r{0}) {
      
        if (4 == sscanf($r, ":%s %d %s %[^\r]", $server, $code, $target, $data)) {

          // Server messages
          // :irc.xxx.net 372 KrokerdilBot :- Excuse of the hour:
          // :irc.xxx.net 353 KrokerdilBot = #test :KrokerdilBot @krokerdil
          // :irc.xxx.net 366 KrokerdilBot #test :End of /NAMES list.
          // :irc.xxx.net 376 KrokerdilBot :End of /MOTD command.
          $this->cat && $this->cat->debugf('    Server message #%d %s', $code, IRCConstants::nameOf($code));
          
          switch ($code) {
            case IRC_REPLY_MOTDSTART:
            case IRC_REPLY_MOTD:
            case IRC_REPLY_ENDOFMOTD:
              $this->notify(substr(IRCConstants::nameOf($code), 6), $server, $target, $data);
              break;
            
            default:
              $this->notify('serverMessage', $server, $code, $target, $data);
          }
        } elseif (4 <= sscanf($r, ":%[^!]!%s %s %s %[^\r]", $nick, $user, $action, $target, $data)) {

          // Other messages
          switch (strtolower($action)) {

            // :krokerdil!<EMAIL> PRIVMSG KrokerdilBot :Wop
            // :krokerdil!<EMAIL> PRIVMSG #test :Poff
            // :krokerdil!<EMAIL> PRIVMSG #test :\001ACTION thinks Bot is stoopid\001
            // :krokerdil!<EMAIL> PRIVMSG Bot :\001USERINFO\001
            // :krokerdil!<EMAIL> PRIVMSG KrokerdilBot :Hello\0033Hello
            case 'privmsg':
              if ("\1" == $data{1}) {
                list($command, $params)= explode(' ', substr($data, 2, -1), 2);
                $this->notify($command, $nick, $target, $params);
              } else {
                $this->notify('privateMessage', $nick, $target, substr($data, 1));
              }
              break;

            // :krokerdil!<EMAIL> JOIN :#test
            case 'join':
              $this->notify('joins', substr($target, 1), $nick);
              break;

            // :krokerdil!<EMAIL> PART #test :Leaving
            case 'part':
              $this->notify('parts', $target, $nick, substr($data, 1));
              break;

            // :krokerdil!<EMAIL> MODE #test +v KrokerdilBot
            // :krokerdil!<EMAIL> MODE #test +k HALLO
            // :kro<EMAIL>dil!<EMAIL> MODE #test -k HALLO
            // :kro<EMAIL>dil!<EMAIL> MODE #test +b *!*<EMAIL>
            // :kro<EMAIL>dil!<EMAIL> MODE #test +o KrokerdilBot 
            case 'mode':
              list($mode, $params)= explode(' ', $data, 2);
              $this->notify('modeChanges', $nick, $target, $mode, $params);
              break;

            // :krokerdil!<EMAIL> KICK #schlund Bot :IHATEYOU
            case 'kick':
              list($who, $reason)= explode(' ', $data, 2);
              $this->notify('kicks', $target, $nick, $who, substr($reason, 1));
              break;

            // :krokerdil!<EMAIL> QUIT :signoff: Ave
            case 'quit':
              $this->notify('quits', $target, $nick, $data);
              break;            

            // :kro<EMAIL>dil!<EMAIL> NICK :schnutepanzerdil
            case 'nick':
              $this->notify('nickChanges', $nick, substr($target, 1));
              break;
            
            // :krokerdil!<EMAIL> INVITE KrokerdilBot :#test
            case 'invite':
              $this->notify('invite', $nick, $target, substr($data, 1));
              break;
            
            // :krokerdil!<EMAIL> NOTICE #test :8tung baby
            // :krokerdil!<EMAIL> NOTICE KrokerdilBot :8tung baby
            case 'notice':
              $this->notify('notice', $nick, $target, substr($data, 1));
              break;
              
            // :krokerdil!<EMAIL> TOPIC #test :Korkerdl             
            case 'topic':
              $this->notify('topic', $nick, $target, substr($data, 1));
              break;

            default:
              $this->cat && $this->cat->warn('Unrecognized action', $action, '(', $nick, $user, $action, $target, $data, ')');
              return FALSE;
          }
        } else {
          $this->cat && $this->cat->error('Could not parse', $r);
          return FALSE;
        }
      } else {
        $this->cat && $this->cat->error('Malformed message', $r);
        return FALSE;
      }
      return TRUE;
    }
    
    /**
     * Set nickname and update this user's nickname.
     *
     * @access  public
     * @param   string nick
     * @return  bool success TRUE in case the command could be sent
     * @throws  lang.IllegalStateException in case the socket is not connected
     */
    function setNick($nick) {
      if (!$this->sock->isConnected()) {
        return throw(new IllegalStateException('Not connected'));
      }
      if (!$this->writeln('NICK %s', $nick)) return FALSE;
      $this->user->setNick($nick);
      return TRUE;
    }
    
    /**
     * Join a channel
     *
     * @access  public
     * @param   string channel without leading #
     * @param   string keyword default NULL
     * @return  bool success TRUE in case the command could be sent
     * @throws  lang.IllegalStateException in case the socket is not connected
     */
    function join($channel, $keyword= NULL) {
      if (!$this->sock->isConnected()) {
        return throw(new IllegalStateException('Not connected'));
      }
      
      // Be tolerant about channel parameter and rip off leading # if necessary
      if ('#' == $channel{0}) $channel= substr($channel, 1);
      return $this->writeln('JOIN #%s %s', $channel, $keyword);
    }
    
    /**
     * Wrapper around writeln() that sends a PRIVMSG
     *
     * @access  public
     * @param   string dest either a nick or a channel (beginning with "#")
     * @param   string format
     * @param   mixed* format_arguments
     * @return  bool success TRUE in case the command could be sent
     */
    function sendMessage() {
      $dest= array_shift($a= func_get_args());
      return $this->writeln('PRIVMSG %s :%s', $dest, vsprintf(array_shift($a), $a));
    }

    /**
     * Wrapper around writeln() that sends a PRIVMSG with an ACTION
     *
     * @access  public
     * @param   string dest either a nick or a channel (beginning with "#")
     * @param   string format
     * @param   mixed* format_arguments
     * @return  bool success TRUE in case the command could be sent
     */
    function sendAction() {
      $dest= array_shift($a= func_get_args());
      return $this->writeln("PRIVMSG %s :\1ACTION %s\1", $dest, vsprintf(array_shift($a), $a));
    }
    
    /**
     * The main loop in which the socket is polled for data, notifying
     * the attached listeners of events.
     *
     * @access  public
     * @throws  lang.IllegalStateException in case the socket is not connected
     * @throws  io.IOException in case registration failed
     */
    function run() {
      if (!$this->sock->isConnected()) {
        return throw(new IllegalStateException('Not connected'));
      }
      
      // Register with the server
      try(); {
        $this->writeln(
          'USER %s %s %s :%s', 
          $this->user->getUsername(),
          $this->user->getHostname(),
          $this->sock->host,
          $this->user->getRealname()
        );

        // Set nickname
        $this->setNick($this->user->getNick());
      } if (catch('IOException', $e)) {
        return throw($e);
      }
      
      // Loop while socket is not disconnected
      $messages= array();
      while (!$this->sock->eof()) {
        if (!$r= $this->readln()) continue;
        
        // Reply to PINGs immediately
        if ('PING :' == substr($r, 0, 6)) {
          $this->writeln('PONG %s', substr($r, 6));
          $this->notify('ping', substr($r, 6));
          continue;
        }

        // Process data
        $this->process($r);
      }
    }
    
    /**
     * Close the communication socket. Issues a QUIT before 
     *
     * @access  public
     * @see     xp://peer.Socket#close
     * @return  bool success
     */
    function close() {
      if ($this->sock->isConnected()) return TRUE;
      
      $this->notify('disconnect', $this->sock->host, $this->sock->port); 
      
      // Politely say goodbye
      $this->writeln('QUIT');
      return $this->sock->close();
    }

    /**
     * Directly read a line off the socket. The returned line has any 
     * trailing CRLF trimmed off.
     *
     * @access  public
     * @see     xp://peer.Socket#readLine
     * @return  string or FALSE to indicate failure
     */    
    function readln() {
      xp::gc();
      try(); {
        $r= $this->sock->readLine(0x2000);
      } if (catch('IOException', $e)) {
        $this->cat && $this->cat->warn($e);
        return FALSE;
      }
      $this->cat && $this->cat->infof(
        '<<< %s %3d: %s', 
        gettype($r), 
        strlen($r), 
        addcslashes($r, "\0..\37")
      );
      
      return $r;
    }
    
    /**
     * Directly write a string to the socket. A CRLF is added automatically 
     * at the string's end.
     *
     * Note: It seems as if 448 bytes is the maximum length of a single
     * string that can be written e.g. as a PRIVMSG (including the PRIVMSG
     * command and destination). This limit is not checked upon.
     *
     * @access  public
     * @see     xp://peer.Socket#write
     * @param   string format
     * @param   mixed* formatargs
     * @return  bool success
     */
    function writeln() {
      $cmd= vsprintf(array_shift($a= func_get_args()), $a);
      $this->cat && $this->cat->infof(
        '>>> %s %3d: %s', 
        gettype($cmd), 
        strlen($cmd), 
        addcslashes($cmd, "\0..\37")
      );
      
      return $this->sock->write($cmd."\r\n");
    }
  } implements(__FILE__, 'util.log.Traceable');
?>

{'fraction_non_alphanumeric': 0.15499943209783063, 'fraction_numerical': 0.006663385454132435, 'mean_word_length': 1.8371643394199786, 'pattern_counts': {'":': 4, '<': 65, '<?xml version=': 0, '>': 273, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '964590', 'n_tokens_mistral': 8651, 'n_tokens_neox': 8239, 'n_words': 2045}