/////////////////////////////////////////////////////////////////////////////// public int SendDiscoInfoRequest(XmppJid to) { string buf = string.Format("<iq from=\"{0}\" to=\"{1}\" id=\"{2}\" type=\"get\"><query xmlns=\"http://jabber.org/protocol/disco#info\"/></iq>", Jid.Value, to.Value, iqCount); sendStream(buf); return(iqCount++); }
/////////////////////////////////////////////////////////////////////////////// public int SendNotification(XmppJid to, string thread, XmppChatStatus notify) { //if (!string.IsNullOrEmpty(thread)) //thread = "<thread>" + thread + "</thread>"; thread = ""; string bufNotify = string.Empty; switch (notify) { case XmppChatStatus.NowComposing: bufNotify = "<composing xmlns=\"http://jabber.org/protocol/chatstates\"/>"; break; case XmppChatStatus.StopComposing: bufNotify = "<paused xmlns=\"http://jabber.org/protocol/chatstates\"/>"; break; case XmppChatStatus.Active: bufNotify = "<active xmlns=\"http://jabber.org/protocol/chatstates\"/>"; break; case XmppChatStatus.Inactive: bufNotify = "<inactive xmlns=\"http://jabber.org/protocol/chatstates\"/>"; break; case XmppChatStatus.Gone: bufNotify = "<gone xmlns=\"http://jabber.org/protocol/chatstates\"/>"; break; } string buf = string.Format("<message to=\"{0}\" type=\"chat\" id=\"{1}\" from=\"{2}\">{3}{4}</message>", to.Value, iqCount, Jid.Value, bufNotify, thread); sendStream(buf); return(iqCount++); }
internal void UpdatePresence(XmppJid jid, Presence presence) { lock (syncObject) { XmppContactResource resource = resources .Where(contactResource => contactResource.ResourceId.Equals(jid)) .SingleOrDefault(); if (resource == null) { resource = new XmppContactResource(session, this, jid); resources.Add(resource); } resource.Update(presence); // Remove the resource information if the contact has gone offline if (!resource.IsDefaultResource && resource.Presence.PresenceStatus == XmppPresenceState.Offline) { resources.Remove(resource); } //NotifyPropertyChanged(() => Presence); //NotifyPropertyChanged(() => Resource); } }
/////////////////////////////////////////////////////////////////////////////// public int SendPing(XmppJid to) { string buf = string.Format("<iq from=\"{0}\" to=\"{1}\" id=\"{2}\" type=\"get\"><ping xmlns=\"urn:xmpp:ping\"/></iq>", Jid.Value, to.Value, iqCount); sendStream(buf); return(iqCount++); }
/// <summary> /// Disposes the specified disposing. /// </summary> /// <param name = "disposing">if set to <c>true</c> [disposing].</param> protected virtual void Dispose(bool disposing) { if (!IsDisposed) { if (disposing) { Close(); } userId = null; connectionString = null; SyncReads = null; SyncWrites = null; onMessageReceivedSubject = null; onXmppStreamInitializedSubject = null; onXmppStreamClosedSubject = null; // Call the appropriate methods to clean up // unmanaged resources here. // If disposing is false, // only the following code is executed. } IsDisposed = true; }
/// <summary> /// Initializes a new instance of the <see cref = "T:XmppChatRoom" /> class. /// </summary> /// <param name = "session">The session.</param> /// <param name = "conferenceService">The conference service.</param> /// <param name = "chatRoomId">The chat room id.</param> internal XmppChatRoom(XmppSession session, XmppService conferenceService, XmppJid chatRoomId) : base(session, chatRoomId) { this.conferenceService = conferenceService; seekEnterChatRoomEvent = new AutoResetEvent(false); createChatRoomEvent = new AutoResetEvent(false); }
/// <summary> /// Initializes a new instance of the <see cref = "XmppServiceDiscoveryObject" /> class. /// </summary> protected XmppServiceDiscoveryObject(XmppSession session, string identifier) { this.session = session; this.identifier = identifier; waitEvent = new AutoResetEvent(false); Subscribe(); }
public void Update(XmppJid jid, Chat ci) { string key = GetKey(jid); if (chats.ContainsKey(key)) { chats.Remove(key); } chats.Add(key, ci); }
/// <summary> /// Subscribes to presence updates of the current user /// </summary> /// <param name = "jid"></param> public void Unsuscribed(XmppJid jid) { var presence = new Presence { Type = PresenceType.Unsubscribed, To = jid }; session.Send(presence); }
public Chat Get(XmppJid jid, string thread) { string key = GetKey(jid); if (chats.ContainsKey(key)) { return(chats[key]); } return(null); }
/// <summary> /// Request subscription to the given user /// </summary> /// <param name = "contactId"></param> public void RequestSubscription(XmppJid jid) { var presence = new Presence { Type = PresenceType.Subscribe, To = jid }; session.Send(presence); }
public virtual void Close() { userId = null; connectionString = null; SyncReads = null; SyncWrites = null; onMessageReceivedSubject = null; onXmppStreamInitializedSubject = null; onXmppStreamClosedSubject = null; }
public RosterItem Get(XmppJid jid) { string key = GetKey(jid); if (rosters.ContainsKey(key)) { return(rosters[key]); } return(null); }
/// <summary> /// Initializes a new instance of the <see cref = "T:XmppContact" /> class. /// </summary> /// <param name = "session">The session.</param> /// <param name = "contactId">The contact id.</param> /// <param name = "name">The name.</param> /// <param name = "subscription">The subscription.</param> /// <param name = "groups">The groups.</param> internal XmppContact(XmppSession session, string contactId, string name, XmppContactSubscriptionType subscription, IList <string> groups) { this.session = session; syncObject = new object(); this.contactId = contactId; resources = new List <XmppContactResource>(); RefreshData(name, subscription, groups); AddDefaultResource(); }
/// <summary> /// Initializes a new instance of the <see cref = "XmppContactResource" /> class. /// </summary> internal XmppContactResource(XmppSession session, XmppContact contact, XmppJid resourceId) { this.session = session; this.contact = contact; this.resourceId = resourceId; presence = new XmppContactPresence(this.session); capabilities = new XmppClientCapabilities(); pendingMessages = new List<string>(); Subscribe(); }
public void Update(XmppJid jid, RosterItem ri) { string key = GetKey(jid); if (rosters.ContainsKey(key)) { rosters.Remove(key); } rosters.Add(key, ri); needRefresh = true; }
/////////////////////////////////////////////////////////////////////////////// public int SendVCardRequest(XmppJid to) { XmppJid from = Jid; if (to.Bare == Jid.Bare) { from = to = null; } //to = new XmppJid(to.Bare, string.Empty); return(SendIq(from, to, XmppIqType.Get, "<vCard xmlns=\"vcard-temp\"/>")); }
/// <summary> /// Sets the initial presence against the given user. /// </summary> /// <param name = "target">JID of the target user.</param> public void SetInitialPresence(XmppJid target) { var presence = new Presence(); if (target != null && target.ToString().Length > 0) { presence.To = target.ToString(); } session.Send(presence); }
/// <summary> /// Initializes a new instance of the <see cref = "XmppContactResource" /> class. /// </summary> internal XmppContactResource(XmppSession session, XmppContact contact, XmppJid resourceId) { this.session = session; this.contact = contact; this.resourceId = resourceId; presence = new XmppContactPresence(this.session); capabilities = new XmppClientCapabilities(); pendingMessages = new List <string>(); Subscribe(); }
/// <summary> /// Gets the presence of the given user. /// </summary> /// <param name = "targetJid">User JID</param> public void GetPresence(XmppJid targetJid) { var presence = new Presence { Id = XmppIdentifierGenerator.Generate(), Type = PresenceType.Probe, From = session.UserId, To = targetJid }; session.Send(presence); }
private Chat SingletonChat(XmppJid to, string thread) { Chat ci = ChatMgr.Get(to, thread); if (ci == null) { RosterItem ri = RosterMgr.Get(to); ci = new Chat(client, ri, thread); ci.Owner = this; ChatMgr.Update(to, ci); } return(ci); }
private void RefreshAndViewVCard(XmppJid jid) { int id = client.SendVCardRequest(jid); Signals.Register(id, (par) => { if (par.Type == XmppIqType.Error) { DialogUtils.ShowWarning(this, string.Format("There was an error recovering vCard from the server. {0} {0} {1}", Environment.NewLine, ParseStreamError(par.xStream))); } else { ViewVCard(new XmppVCard(jid, par.xStream)); } }); }
internal void AddDefaultResource() { var defaultPresence = new Presence(); var contactResource = new XmppContactResource(session, this, ContactId); var resourceJid = new XmppJid(contactId.UserName, ContactId.DomainName, Guid.NewGuid().ToString()); // Add a default resource defaultPresence.TypeSpecified = true; defaultPresence.From = resourceJid; defaultPresence.Type = PresenceType.Unavailable; defaultPresence.Items.Add(XmppContactResource.DefaultPresencePriorityValue); contactResource.Update(defaultPresence); resources.Add(contactResource); }
/// <summary> /// Creates the chat room. /// </summary> /// <param name = "chatRoomName">Name of the chat room.</param> /// <returns></returns> public XmppChatRoom EnterChatRoom(string chatRoomName) { CheckSessionState(); XmppService service = ServiceDiscovery.GetService(XmppServiceCategory.Conference); XmppChatRoom chatRoom = null; var chatRoomId = new XmppJid ( chatRoomName, service.Identifier, UserId.UserName ); if (service != null) { chatRoom = new XmppChatRoom(this, service, chatRoomId); chatRoom.Enter(); } return(chatRoom); }
/// <summary> /// Creates the chat. /// </summary> /// <param name = "contactId">The contact id.</param> /// <returns></returns> public XmppChat CreateChat(XmppJid contactId) { CheckSessionState(); XmppChat chat = null; lock (syncObject) { if (!chats.ContainsKey(contactId.BareIdentifier)) { chat = new XmppChat(this, Roster[contactId.BareIdentifier]); chats.Add(contactId.BareIdentifier, chat); chat.ChatClosed += OnChatClosed; } else { chat = chats[contactId.BareIdentifier]; } } return(chat); }
/// <summary> /// Deletes a user from the roster list /// </summary> public XmppRoster RemoveContact(XmppJid jid) { var query = new RosterQuery(); var iq = new IQ(); iq.ID = XmppIdentifierGenerator.Generate(); iq.Type = IQType.Set; iq.From = connection.UserId; var item = new RosterItem(); item.Jid = jid.BareIdentifier; item.Subscription = RosterSubscriptionType.Remove; query.Items.Add(item); iq.Items.Add(query); pendingMessages.Add(iq.ID); connection.Send(iq); return(this); }
/// <summary> /// Checks if a given user has an open chat session /// </summary> /// <param name = "contactId"></param> /// <returns></returns> public bool HasOpenChat(XmppJid contactId) { return HasOpenChat(contactId.BareIdentifier); }
/// <summary> /// Creates the chat room. /// </summary> /// <param name = "chatRoomName">Name of the chat room.</param> /// <returns></returns> public XmppChatRoom EnterChatRoom(string chatRoomName) { CheckSessionState(); XmppService service = ServiceDiscovery.GetService(XmppServiceCategory.Conference); XmppChatRoom chatRoom = null; var chatRoomId = new XmppJid ( chatRoomName, service.Identifier, UserId.UserName ); if (service != null) { chatRoom = new XmppChatRoom(this, service, chatRoomId); chatRoom.Enter(); } return chatRoom; }
private void OnPresenceMessageReceived(Presence message) { XmppJid jid = message.From; if (jid.BareIdentifier == session.UserId.BareIdentifier) { #warning TODO: See how to handle our own presence changes from other resources } else { XmppContact contact = this[jid.BareIdentifier]; if (contact != null) { if (message.TypeSpecified) { switch (message.Type) { case PresenceType.Probe: break; case PresenceType.Subscribe: // auto-accept subscription requests session.Presence.Subscribed(jid); break; case PresenceType.Unavailable: contact.UpdatePresence(jid, message); break; case PresenceType.Unsubscribe: session.Presence.Unsuscribed(jid); break; } } else { contact.UpdatePresence(jid, message); } } else { if (message.TypeSpecified) { switch (message.Type) { case PresenceType.Probe: break; case PresenceType.Subscribe: // auto-accept subscription requests session.Presence.Subscribed(jid); break; case PresenceType.Unsubscribe: session.Presence.Unsuscribed(jid); break; } } } } }
private string GetKey(XmppJid jid) { return(jid.Bare); }
public bool Contains(XmppJid jid) { string key = GetKey(jid); return(rosters.ContainsKey(key)); }
private void doParseStream(string stream) { XmppIQ xStream; try { xStream = XmppIQ.Parse(stream); } catch (Exception) { //MessageBox.Show("Invalid stream :(\n\n" + stream); return; } switch (xStream.Name) { #region PRESENCE case "presence": { XmppPresence presence = new XmppPresence(); string type = xStream.GetAttribute("type"); presence.From = new XmppJid(xStream.GetAttribute("from")); presence.To = new XmppJid(xStream.GetAttribute("to")); // # type == "" if (string.IsNullOrEmpty(type)) { XmppIQ xStatus = xStream.FindDescendant("status"); if (xStatus != null) { presence.MessageStatus = xStatus.Text; } XmppIQ xShow = xStream.FindDescendant("show"); presence.Status = xShow != null?XmppIq.StrToPresence(xShow.Text) : XmppPresenceStatus.Online; XmppIQ xCard = xStream.FindDescendant("x", "vcard-temp:x:update"); if (xCard != null) { XmppIQ xPhoto = xCard.FindDescendant("photo", "vcard-temp:x:update"); if (xPhoto != null) { presence.PhotoHash = xPhoto.Text; } } if (Presence != null) { Presence(this, new XmppPresenceEventArgs(presence)); } } // # type == "unavailable" else if (type == "unavailable") { presence.Status = XmppPresenceStatus.Unavailable; if (Presence != null) { Presence(this, new XmppPresenceEventArgs(presence)); } } // # type == "probe" // probe request from server else if (type == "probe") { SetPresence(statePresence, string.Empty, null); } // # type == "subscribe" // new subscription request else if (type == "subscribe") { if (SubscribeRequest != null) { SubscribeRequest(this, new XmppSubscribeRequestEventArgs(presence.From)); } } // # type == "error" // presence stanza error else if (type == "error") { // @@@ } } break; #endregion #region MESSAGE case "message": { XmppMessage message = new XmppMessage(); message.Type = xStream.GetAttribute("type"); message.From = new XmppJid(xStream.GetAttribute("from")); message.To = new XmppJid(xStream.GetAttribute("to")); Int32.TryParse(xStream.GetAttribute("id"), out message.ID); if (message.Type != "error") { // # user composing new message if (xStream.FindDescendant("composing", "http://jabber.org/protocol/chatstates") != null) { if (ChatNotify != null) { ChatNotify(this, new XmppChatNotifyEventArgs(message.From, string.Empty, XmppChatStatus.NowComposing)); } } // # user stop composing else if (xStream.FindDescendant("paused", "http://jabber.org/protocol/chatstates") != null) { if (ChatNotify != null) { ChatNotify(this, new XmppChatNotifyEventArgs(message.From, string.Empty, XmppChatStatus.StopComposing)); } } // # user is inactive else if (xStream.FindDescendant("inactive", "http://jabber.org/protocol/chatstates") != null) { if (ChatNotify != null) { ChatNotify(this, new XmppChatNotifyEventArgs(message.From, string.Empty, XmppChatStatus.Inactive)); } } // # user has left conversation else if (xStream.FindDescendant("gone", "http://jabber.org/protocol/chatstates") != null) { if (ChatNotify != null) { ChatNotify(this, new XmppChatNotifyEventArgs(message.From, string.Empty, XmppChatStatus.Gone)); } } // # user is active ;) else if (xStream.FindDescendant("active", "http://jabber.org/protocol/chatstates") != null) { if (ChatNotify != null) { ChatNotify(this, new XmppChatNotifyEventArgs(message.From, string.Empty, XmppChatStatus.Active)); } } // # check for new message XmppIQ xBody = xStream.FindDescendant("body"); if (xBody != null) { message.Body = xBody.Text; XmppIQ xThread = xStream.FindDescendant("thread"); if (xThread != null) { message.Thread = xThread.Text; } XmppIQ xSubject = xStream.FindDescendant("subject"); if (xSubject != null) { message.Subject = xSubject.Text; } if (Message != null) { Message(this, new XmppMessageEventArgs(message)); } } } } break; #endregion #region IQ case "iq": { XmppIQ xQuery = null; string type = xStream.GetAttribute("type"); // Roster xQuery = xStream.FindDescendant("query", "jabber:iq:roster"); if (xQuery != null) { List <XmppRosterItem> roster = new List <XmppRosterItem>(); foreach (XmppIQ xItem in xQuery.Children()) { if (xItem.Name != "item") { continue; } string jid = xItem.GetAttribute("jid"); string sub = xItem.GetAttribute("subscription"); string ask = xItem.GetAttribute("ask"); string name = xItem.GetAttribute("name"); string group = xItem.GetAttribute("group"); roster.Add(new XmppRosterItem(jid, sub, name, group, ask)); } if (Roster != null) { Roster(this, new XmppRosterEventArgs(roster, XmppIq.StrToType(type))); } } // Disco xQuery = xStream.FindDescendant("query", "http://jabber.org/protocol/disco#info"); if (xQuery != null) { if (type == "get") { XmppJid from = new XmppJid(xStream.GetAttribute("from")); string id = xStream.GetAttribute("id"); sendStream(new XmppIq().ToStream(Jid, from, XmppIqType.Result, id, Disco.ToStreamResult())); // @@@@@ ottimizza SendIq(...) //SendIq(from, XmppIqType.Result, Disco.ToStreamResult());// @@@@@ ottimizza SendIq(...) } } // vCard xQuery = xStream.FindDescendant("vCard", "vcard-temp"); if (xQuery != null) { if (type == "result") { XmppJid from = new XmppJid(xStream.GetAttribute("from")); XmppVCard vCard = new XmppVCard(from, xQuery); if (VCard != null) { VCard(this, new XmppVCartEventArgs(vCard)); } } } // Ping xQuery = xStream.FindDescendant("ping", "urn:xmpp:ping"); if (xQuery != null) { if (type == "get") { XmppJid from = new XmppJid(xStream.GetAttribute("from")); string id = xStream.GetAttribute("id"); sendStream(new XmppIq().ToStream(Jid, from, XmppIqType.Result, id, string.Empty)); // @@@@@ ottimizza SendIq(...) } } } break; #endregion } Signal(xStream); }
private void doAuthenticate() { doChangeState(XmppEngineState.Opening); if (tcpClient != null) { tcpClient.Dispose(); tcpClient = null; } try { tcpClient = new XmppTcpSocket(HostName, HostPort); } catch (Exception ex) { if (Error != null) { Error(this, new XmppErrorEventArgs(XmppErrorType.SocketError, string.Empty, ex.GetType().ToString() + ": " + ex.Message)); } if (tcpClient != null) { tcpClient.Dispose(); tcpClient = null; } return; } string stream; XmppIQ xStream; // ========================================================================== // === START TLS LAYER ====================================================== stream = doRequestFeatures(); if (stream.Contains("<starttls")) { sendStream("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>"); stream = recvStream(); if (stream.Contains("<proceed")) { bool sslStarted = false; string message = string.Empty; try { sslStarted = tcpClient.StartSSL(); sslStarted = true; } catch (Exception ex) { sslStarted = false; message = string.Format("{0}: {1}", ex.GetType(), ex.Message); } if (!sslStarted) { if (Error != null) { Error(this, new XmppErrorEventArgs(XmppErrorType.SocketStartSSLError, string.Empty, message)); } return; } stream = doRequestFeatures(); } } if (stream.Contains("<mechanism>DIGEST-MD5</mechanism>")) { // ========================================================================== // === SASL DIGEST-MD5 AUTHENTICATION PROCESS =============================== DigestMD5MechanismProvider mech = new DigestMD5MechanismProvider(UserName, UserPassword); sendStream(mech.GetAuthStream()); stream = recvStream(); xStream = XmppIQ.Parse(stream); if (!xStream.Equal("challenge", "urn:ietf:params:xml:ns:xmpp-sasl")) { if (Error != null) { string tag, message; XmppError.Parse(XmppIQ.Parse(stream), out tag, out message); Error(this, new XmppErrorEventArgs(XmppErrorType.AuthUserNotAuthorized, tag, message)); } return; } sendStream(mech.GetChallengeResponse(xStream.Text)); stream = recvStream(); xStream = XmppIQ.Parse(stream); if (!xStream.Equal("challenge", "urn:ietf:params:xml:ns:xmpp-sasl")) { if (Error != null) { string tag, message; XmppError.Parse(XmppIQ.Parse(stream), out tag, out message); Error(this, new XmppErrorEventArgs(XmppErrorType.AuthUserNotAuthorized, tag, message)); } return; } sendStream("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"); } else if (stream.Contains("<mechanism>PLAIN</mechanism>")) { // ========================================================================== // === SASL PLAIN AUTHENTICATION PROCESS ==================================== PlainMechanismProvider mech = new PlainMechanismProvider(UserName, UserPassword); sendStream(mech.GetAuthStream()); } else if (stream.Contains("<mechanism>X-GOOGLE-TOKEN</mechanism>")) { // ========================================================================== // === SASL X-GOOGLE-TOKEN AUTHENTICATION PROCESS ==================================== XGoogleTokenMechanismProvider mech = new XGoogleTokenMechanismProvider(UserName, UserPassword, UserResource); sendStream(mech.GetAuthStream()); } else if (stream.Contains("<mechanism>X-MESSENGER-OAUTH2</mechanism>")) { // ========================================================================== // === SASL X-MESSENGER-OAUTH2 AUTHENTICATION PROCESS ==================================== XMessengerOAuth2MechanismProvider mech = new XMessengerOAuth2MechanismProvider(UserName, UserOAuthToken); sendStream(mech.GetAuthStream()); } else if (stream.Contains("<mechanism>X-FACEBOOK-PLATFORM</mechanism>")) { // ========================================================================== // === SASL X-FACEBOOK-PLATFORM AUTHENTICATION PROCESS ==================================== XFacebookPlatformMechanismProvider mech = new XFacebookPlatformMechanismProvider(UserName, UserOAuthToken); sendStream(mech.GetAuthStream()); stream = recvStream(); xStream = XmppIQ.Parse(stream); if (!xStream.Equal("challenge", "urn:ietf:params:xml:ns:xmpp-sasl")) { if (Error != null) { string tag, message; XmppError.Parse(XmppIQ.Parse(stream), out tag, out message); Error(this, new XmppErrorEventArgs(XmppErrorType.AuthUserNotAuthorized, tag, message)); } return; } sendStream(mech.GetChallengeResponse(xStream.Text)); } else { if (Error != null) { Error(this, new XmppErrorEventArgs(XmppErrorType.AuthModeNotSupported)); } return; } stream = recvStream(); if (stream.Contains("<failure")) { if (Error != null) { string tag, message; XmppError.Parse(XmppIQ.Parse(stream), out tag, out message); Error(this, new XmppErrorEventArgs(XmppErrorType.AuthUserNotAuthorized, tag, message)); } return; } // ========================================================================== // === BIND RESOURCE ======================================================== stream = doRequestFeatures(); SendIq(Jid, null, XmppIqType.Set, "<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"><resource>" + UserResource + "</resource></bind>"); stream = recvStream(); xStream = new XmppIQ(stream); XmppIQ xJid = xStream.FindDescendant("jid", "urn:ietf:params:xml:ns:xmpp-bind"); if (xJid != null) { Jid = new XmppJid(xJid.Text); } // ========================================================================== // === START SESSION ======================================================== SendIq(Jid, null, XmppIqType.Set, "<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>"); stream = recvStream(); // ========================================================================== // === OK: AUTHENTICATED IN JABBER NETWORK, You rocks! ====================== //tcpClient.SetReadTimeout(0); doChangeState(XmppEngineState.Open); if (UserConnected != null) { UserConnected(this, new XmppEventArgs(Jid.Value)); } }
/// <summary> /// Creates the chat. /// </summary> /// <param name = "contactId">The contact id.</param> /// <returns></returns> public XmppChat CreateChat(XmppJid contactId) { CheckSessionState(); XmppChat chat = null; lock (syncObject) { if (!chats.ContainsKey(contactId.BareIdentifier)) { chat = new XmppChat(this, Roster[contactId.BareIdentifier]); chats.Add(contactId.BareIdentifier, chat); chat.ChatClosed += OnChatClosed; } else { chat = chats[contactId.BareIdentifier]; } } return chat; }