context: /* --------------------------------------------------------------------------
   * Copyrights
   *
   * Portions created by or assigned to Cursive Systems, Inc. are
   * Copyright (c) 2002-2008 Cursive Systems, Inc.  All Rights Reserved.  Contact
   * information for Cursive Systems, Inc. is available at
   * http://www.cursive.net/.
   *
   * License
   *
   * Jabber-Net is licensed under the LGPL.
   * See LICENSE.txt for details.
   * --------------------------------------------------------------------------*/
  
  using System;
  using System.ComponentModel;
  using System.Collections;
  using System.Diagnostics;
  using System.Xml;
  using bedrock.util;
  using bedrock.net;
  using jabber.connection;
  using jabber.protocol;
  using jabber.protocol.client;
  using jabber.protocol.iq;
  using jabber.connection.sasl;
  
  namespace jabber.client
  {
      /// <summary>
      /// Informs the client that a presence packet has been received.
      /// </summary>
      public delegate void PresenceHandler(Object sender, Presence pres);
  
      /// <summary>
      /// Informst the client that a message has been received.
      /// </summary>
      public delegate void MessageHandler(Object sender, Message msg);
  
      /// <summary>
      /// Informs the client that an IQ has been received.
      /// </summary>
      public delegate void IQHandler(Object sender, IQ iq);
  
      /// <summary>
      /// Need more information for registration.  Return false to cancel.
      /// </summary>
      public delegate bool RegisterInfoHandler(Object sender, Register register);
  
      /// <summary>
      /// Provides a component for clients to use to access the XMPP server.
      /// You can install this in your Toolbox, drop onto a form, a service, and so on.
      /// This class hooks into the OnProtocol event and calls the Connect() method.
      /// </summary>
      [SVN(@"$Id$")]
      public class JabberClient : XmppStream
      {
          private static readonly object[][] DEFAULTS = new object[][]
          {
              new object[] {Options.RESOURCE, "Jabber.Net"},
              new object[] {Options.PRIORITY, 0},
              new object[] {Options.AUTO_LOGIN, true},
              new object[] {Options.AUTO_ROSTER, true},
              new object[] {Options.AUTO_IQ_ERRORS, true},
              new object[] {Options.AUTO_PRESENCE, true},
              new object[] {Options.PROXY_PORT, 1080},
              new object[] {Options.SRV_PREFIX, "_xmpp-client._tcp."},
          };
  
          private void init()
          {
              SetDefaults(DEFAULTS);
  
              this.OnSASLStart += new jabber.connection.sasl.SASLProcessorHandler(JabberClient_OnSASLStart);
              this.OnSASLEnd += new jabber.protocol.stream.FeaturesHandler(JabberClient_OnSASLEnd);
              this.OnSASLError += new ProtocolHandler(JabberClient_OnSASLError);
              this.OnStreamInit += new StreamHandler(JabberClient_OnStreamInit);
          }
  
          /// <summary>
          /// Creates a new Jabber client and associates it with the parent window.
          /// Required for Windows.Forms Class Composition Designer support
          /// </summary>
          /// <param name="container">Parent container.</param>
          public JabberClient(System.ComponentModel.IContainer container) :
              base(container)
          {
              init();
          }
  
          /// <summary>
          /// Creates a new JabberClient.
          /// Required for Windows.Forms Class Composition Designer support.
          /// </summary>
          public JabberClient() : base()
          {
              init();
          }
  
          /*
          /// <summary>
          /// Create a new JabberClient, reusing an existing SocketWatcher.
          /// </summary>
          /// <param name="watcher">SocketWatcher to use.</param>
          public JabberClient(SocketWatcher watcher) : base(watcher)
          {
              init();
          }
          */
  
          /// <summary>
          /// Informs the client that it received a presence packet.
          /// </summary>
          [Category("Protocol")]
          [Description("We received a presence packet.")]
          public event PresenceHandler OnPresence;
  
          /// <summary>
          /// Informs the client that it received a message packet.
          /// </summary>
          [Category("Protocol")]
          [Description("We received a message packet.")]
          public event MessageHandler OnMessage;
  
          /// <summary>
          /// Informs the client that it received an IQ packet.
          /// </summary>
          [Category("Protocol")]
          [Description("We received an IQ packet.")]
          public event IQHandler OnIQ;
  
          /// <summary>
          /// Informs the client that the authentication has failed. The connection is not
          /// terminated if there is an authentication error, and there
          /// is at least one event handler for this event.
          /// </summary>
          [Category("Protocol")]
          [Description("Authentication failed.")]
          public event ProtocolHandler OnAuthError;
  
          /// <summary>
          /// Informs the client that the presence is about to be sent.
          /// This gives a chance to modify outbound presence (fore example, entity caps).
          /// </summary>
          [Category("Protocol")]
          [Description("Presence is about to be sent.  This gives a chance to modify outbound presence (e.g. entity caps)")]
          public event PresenceHandler OnBeforePresenceOut;
  
          /// <summary>
          /// Informs the client that the presence has been sent.
          /// This gives a chance to send presence to other things, such as chat rooms.
          /// </summary>
          [Category("Protocol")]
          [Description("Informs the client that the presence has been sent.  This gives a chance to send presence to other things, such as chat rooms.")]
          public event PresenceHandler OnAfterPresenceOut;
  
          /// <summary>
          /// Determines if SutoLogin is false, and if it is time to log in.
          /// This callback will receive the results of the IQ type=get
          /// in the jabber:iq:auth namespace.  When login is completed,
          /// IsConnected property is set to true.  If there is a login error, the
          /// FireAuthError() method is called.
          /// </summary>
          [Category("Protocol")]
          [Description("AutoLogin is false, and it's time to log in.")]
          public event bedrock.ObjectHandler OnLoginRequired;
  
          /// <summary>
          /// Informs the client if the registration succeeded or failed.
          /// </summary>
          [Category("Protocol")]
          [Description("After calling Register(), the registration succeeded or failed.")]
          public event IQHandler OnRegistered;
  
          /// <summary>
          /// Allows the user to enter registration requested information before sending to the XMPP server.
          ///
          /// WARNING: Make sure you do not return from this handler until the IQ is filled in.
          /// It is now safe to call UI elements, since this callback is now on the GUI thread if
          /// the InvokeControl is set.
          /// </summary>
          [Category("Protocol")]
          [Description("After calling Register, information about the user is required.")]
          public event RegisterInfoHandler OnRegisterInfo;
  
          /// <summary>
          /// Retrieves/Sets the username to connect as.
          /// </summary>
          [Description("The username to connect as.")]
          [Category("Jabber")]
          public string User
          {
              get { return this[Options.USER] as string; }
              set { this[Options.USER] = value; }
          }
  
          /// <summary>
          /// Gets the priority for this connection.
          /// </summary>
          [Description("Priority for this connection.")]
          [Category("Jabber")]
          [DefaultValue(0)]
          public int Priority
          {
              get { return (int) this[Options.PRIORITY]; }
              set { this[Options.PRIORITY] = value; }
          }
  
          /// <summary>
          /// Gets or sets the password to use for connecting to the XMPP server.
          /// This may be sent across the wire plaintext if the XMPP
          /// server doesn't support digest and PlaintextAuth is set to true.
          /// </summary>
          [Description("The password to use for connecting.  " +
                       "This may be sent across the wire plaintext, " +
                       "if the server doesn't support digest, " +
                       "and PlaintextAuth is true")]
          [Category("Jabber")]
          [PasswordPropertyText]
          public string Password
          {
              get { return this[Options.PASSWORD] as string; }
              set { this[Options.PASSWORD] = value; }
          }
  
          /// <summary>
          /// Allows auto-login to be used for the connection to the XMPP server if set to true.
          /// </summary>
          [Description("Automatically log in on connection.")]
          [DefaultValue(true)]
          [Category("Automation")]
          public bool AutoLogin
          {
              get { return (bool) this[Options.AUTO_LOGIN]; }
              set { this[Options.AUTO_LOGIN] = value; }
          }
  
          /// <summary>
          /// Retrieves the roster on connection.
          /// </summary>
          [Description("Retrieves the roster on connection.")]
          [DefaultValue(true)]
          [Category("Automation")]
          public bool AutoRoster
          {
              get { return (bool) this[Options.AUTO_ROSTER]; }
              set { this[Options.AUTO_ROSTER] = value; }
          }
  
          /// <summary>
          /// Sends 501/feature-not-implemented back to the client when an IQ
          /// has not been handled if set to true.
          /// </summary>
          [Description("Automatically send back 501/feature-not-implemented to IQs that have not been handled.")]
          [DefaultValue(true)]
          [Category("Automation")]
          public bool AutoIQErrors
          {
              get { return (bool) this[Options.AUTO_IQ_ERRORS]; }
              set { this[Options.AUTO_IQ_ERRORS] = value; }
          }
  
          /// <summary>
          /// Sends presence information when the connection has been established
          /// if set to true.
          /// </summary>
          [Description("Automatically send presence on connection.")]
          [DefaultValue(true)]
          [Category("Automation")]
          public bool AutoPresence
          {
              get { return (bool) this[Options.AUTO_PRESENCE]; }
              set { this[Options.AUTO_PRESENCE] = value; }
          }
  
          /// <summary>
          /// Gets or sets the connecting resource.
          /// Used to identify a unique connection.
          /// </summary>
          [Description("Gets or sets the connecting resource.  " +
                       "Used to identify a unique connection.")]
          [DefaultValue("Jabber.Net")]
          [Category("Jabber")]
          public string Resource
          {
              get { return this[Options.RESOURCE] as string; }
              set { this[Options.RESOURCE] = value; }
          }
  
          /// <summary>
          /// Gets the stream namespace for this connection.
          /// </summary>
          [Browsable(false)]
          protected override string NS
          {
              get { return URI.CLIENT; }
          }
  
          /// <summary>
          /// Are we currently connected?
          /// </summary>
          [Browsable(false)]
          [DefaultValue(false)]
          public override bool IsAuthenticated
          {
              get { return base.IsAuthenticated; }
              set
              {
                  base.IsAuthenticated = value;
                  if (value)
                  {
                      if (AutoRoster)
                          GetRoster();
                      if (AutoPresence)
                          Presence(PresenceType.available,
                              "online", null, Priority);
                  }
              }
          }
  
          /// <summary>
          /// Connects to the XMPP server.  This happens asynchronously, and
          /// could take a couple of seconds to get the full handshake
          /// completed.  This will authenticate, send presence, and request
          /// roster info, if the Auto* properties are set.
          /// </summary>
          public override void Connect()
          {
              this[Options.AUTO_LOGIN_THISPASS] = this[Options.AUTO_LOGIN];
              this[Options.SERVER_ID] = this[Options.TO];
              base.Connect();
          }
  
          /// <summary>
          /// Closes down the connection.
          /// </summary>
          public override void Close()
          {
              if (IsAuthenticated)
              {
                  Presence p = new Presence(Document);
                  p.Type = PresenceType.unavailable;
                  p.Status = "offline";
                  Write(p);
              }
              base.Close();
          }
  
          /// <summary>
          /// Initiates the authentication process.
          /// </summary>
          public void Login()
          {
              Debug.Assert(User != null, "Username must not be null for XEP-78 authentication");
              Debug.Assert(Password != null, "Password must not be null for XEP-78 authentication");
              Debug.Assert(Resource != null, "Resource must not be null for XEP-78 authentication");
  
              this[Options.AUTO_LOGIN_THISPASS] = true;
  
              if (State == ManualSASLLoginState.Instance)
              {
                  ProcessFeatures();
                  return;
              }
  
              this[Options.JID] = new JID(User, Server, Resource);
  
              AuthIQ aiq = new AuthIQ(Document);
              aiq.Type = IQType.get;
              Auth a = aiq.Instruction;
              a.Username = User;
  
              lock (StateLock)
              {
                  State = GetAuthState.Instance;
              }
              Tracker.BeginIQ(aiq, new IqCB(OnGetAuth), null);
          }
  
          /// <summary>
          /// Sends a presence packet to the XMPP server.
          /// </summary>
          /// <param name="t">The type of presence.</param>
          /// <param name="status">Determines the status of the presence.</param>
          /// <param name="show">Shows the available, away, dnd and so on status.</param>
          /// <param name="priority">Prioritizes this connection.
          /// Higher number mean higher priority. 0 minumum, 127 max.
          /// -1 means this is a presence-only connection.</param>
          public void Presence(PresenceType t,
              string status,
              string show,
              int priority)
          {
              if (IsAuthenticated)
              {
                  if ((priority < -128) || (priority > 127))
                  {
                      throw new ArgumentException("Priority must be -128 to 127", "priority");
                  }
  
                  Presence p = new Presence(Document);
                  if (status != null)
                      p.Status = status;
                  if (t != PresenceType.available)
                  {
                      p.Type = t;
                  }
                  if (show != null)
                      p.Show = show;
                  p.Priority = priority.ToString();
  
                  if (OnBeforePresenceOut != null)
                      OnBeforePresenceOut(this, p);
                  Write(p);
                  if (OnAfterPresenceOut != null)
                      OnAfterPresenceOut(this, p);
              }
              else
              {
                  throw new InvalidOperationException("Client must be authenticated before sending presence.");
              }
          }
  
          /// <summary>
          /// Sends a certain type of message packet to another user.
          /// </summary>
          /// <param name="t">The type of message.</param>
          /// <param name="to">The JID to send the message to.</param>
          /// <param name="body">The body of the message.</param>
          public void Message(MessageType t,
              string to,
              string body)
          {
              if (IsAuthenticated)
              {
                  Message msg = new Message(Document);
                  msg.Type = t;
                  msg.To = to;
                  msg.Body = body;
                  Write(msg);
              }
              else
              {
                  throw new InvalidOperationException("Client must be authenticated before sending messages.");
              }
          }
  
          /// <summary>
          /// Sends a message packet to another user
          /// </summary>
          /// <param name="to">The JID to send the message to.</param>
          /// <param name="body">The body of the message.</param>
          public void Message(
              string to,
              string body)
          {
              Message(MessageType.chat, to, body);
          }
  
          /// <summary>
          /// Requests a new copy of the roster.
          /// </summary>
          public void GetRoster()
          {
              if (IsAuthenticated)
              {
                  RosterIQ riq = new RosterIQ(Document);
                  riq.Type = IQType.get;
                  Write(riq);
              }
              else
              {
                  throw new InvalidOperationException("Client must be authenticated before getting roster.");
              }
          }
  
  
          /// <summary>
          /// Sends a presence subscription request and updates the roster
          /// for a new roster contact.
          /// </summary>
          /// <param name="to">The JID of the contact (required)</param>
          /// <param name="nickname">The nickname to show for the contact.</param>
          /// <param name="groups">A list of groups to put the contact in.  May be null.  Hint: new string[] {"foo", "bar"}</param>
          public void Subscribe(JID to, string nickname, string[] groups)
          {
              Debug.Assert(to != null);
  
              RosterIQ riq = new RosterIQ(Document);
              riq.Type = IQType.set;
              Roster r = riq.Instruction;
              Item i = r.AddItem();
              i.JID = to;
  
gt:             if (nickname != null)