// ---------------------------------------------------------------------------------------------------------------------- // The Photon Chat Api enables clients to connect to a chat server and communicate with other clients. // ChatClient is the main class of this api. // Photon Chat Api - Copyright (C) 2014 Exit Games GmbH // ---------------------------------------------------------------------------------------------------------------------- #if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER #define SUPPORTED_UNITY #endif namespace Photon.Chat { using System; using System.Collections.Generic; using System.Diagnostics; using ExitGames.Client.Photon; #if SUPPORTED_UNITY || NETFX_CORE using Hashtable = ExitGames.Client.Photon.Hashtable; using SupportClass = ExitGames.Client.Photon.SupportClass; #endif /// Central class of the Photon Chat API to connect, handle channels and messages. /// /// This class must be instantiated with a IChatClientListener instance to get the callbacks. /// Integrate it into your game loop by calling Service regularly. If the target platform supports Threads/Tasks, /// set UseBackgroundWorkerForSending = true, to let the ChatClient keep the connection by sending from /// an independent thread. /// /// Call Connect with an AppId that is setup as Photon Chat application. Note: Connect covers multiple /// messages between this client and the servers. A short workflow will connect you to a chat server. /// /// Each ChatClient resembles a user in chat (set in Connect). Each user automatically subscribes a channel /// for incoming private messages and can message any other user privately. /// Before you publish messages in any non-private channel, that channel must be subscribed. /// /// PublicChannels is a list of subscribed channels, containing messages and senders. /// PrivateChannels contains all incoming and sent private messages. /// public class ChatClient : IPhotonPeerListener { const int FriendRequestListMax = 1024; /// Default maximum value possible for when is enabled public const int DefaultMaxSubscribers = 100; private const byte HttpForwardWebFlag = 0x01; /// Enables a fallback to another protocol in case a connect to the Name Server fails. /// /// When connecting to the Name Server fails for a first time, the client will select an alternative /// network protocol and re-try to connect. /// /// The fallback will use the default Name Server port as defined by ProtocolToNameServerPort. /// /// The fallback for TCP is UDP. All other protocols fallback to TCP. /// public bool EnableProtocolFallback { get; set; } /// The address of last connected Name Server. public string NameServerAddress { get; private set; } /// The address of the actual chat server assigned from NameServer. Public for read only. public string FrontendAddress { get; private set; } /// Region used to connect to. Currently all chat is done in EU. It can make sense to use only one region for the whole game. private string chatRegion = "EU"; /// Settable only before you connect! Defaults to "EU". public string ChatRegion { get { return this.chatRegion; } set { this.chatRegion = value; } } /// Current state of the ChatClient. Also use CanChat. public ChatState State { get; private set; } /// Disconnection cause. Check this inside . public ChatDisconnectCause DisconnectedCause { get; private set; } /// /// Checks if this client is ready to send messages. /// public bool CanChat { get { return this.State == ChatState.ConnectedToFrontEnd && this.HasPeer; } } /// /// Checks if this client is ready to publish messages inside a public channel. /// /// The channel to do the check with. /// Whether or not this client is ready to publish messages inside the public channel with the specified channelName. public bool CanChatInChannel(string channelName) { return this.CanChat && this.PublicChannels.ContainsKey(channelName) && !this.PublicChannelsUnsubscribing.Contains(channelName); } private bool HasPeer { get { return this.chatPeer != null; } } /// The version of your client. A new version also creates a new "virtual app" to separate players from older client versions. public string AppVersion { get; private set; } /// The AppID as assigned from the Photon Cloud. public string AppId { get; private set; } /// Settable only before you connect! public AuthenticationValues AuthValues { get; set; } /// The unique ID of a user/person, stored in AuthValues.UserId. Set it before you connect. /// /// This value wraps AuthValues.UserId. /// It's not a nickname and we assume users with the same userID are the same person. public string UserId { get { return (this.AuthValues != null) ? this.AuthValues.UserId : null; } private set { if (this.AuthValues == null) { this.AuthValues = new AuthenticationValues(); } this.AuthValues.UserId = value; } } /// If greater than 0, new channels will limit the number of messages they cache locally. /// /// This can be useful to limit the amount of memory used by chats. /// You can set a MessageLimit per channel but this value gets applied to new ones. /// /// Note: /// Changing this value, does not affect ChatChannels that are already in use! /// public int MessageLimit; /// Limits the number of messages from private channel histories. /// /// This is applied to all private channels on reconnect, as there is no explicit re-joining private channels.
/// Default is -1, which gets available messages up to a maximum set by the server.
/// A value of 0 gets you zero messages.
/// The server's limit of messages may be lower. If so, the server's value will overrule this.
///
public int PrivateChatHistoryLength = -1; /// Public channels this client is subscribed to. public readonly Dictionary PublicChannels; /// Private channels in which this client has exchanged messages. public readonly Dictionary PrivateChannels; // channels being in unsubscribing process // items will be removed on successful unsubscription or subscription (the latter required after attempt to unsubscribe from not existing channel) private readonly HashSet PublicChannelsUnsubscribing; private readonly IChatClientListener listener = null; /// The Chat Peer used by this client. public ChatPeer chatPeer = null; private const string ChatAppName = "chat"; private bool didAuthenticate; private int? statusToSetWhenConnected; private object messageToSetWhenConnected; private int msDeltaForServiceCalls = 50; private int msTimestampOfLastServiceCall; /// Defines if a background thread will call SendOutgoingCommands, while your code calls Service to dispatch received messages. /// /// The benefit of using a background thread to call SendOutgoingCommands is this: /// /// Even if your game logic is being paused, the background thread will keep the connection to the server up. /// On a lower level, acknowledgements and pings will prevent a server-side timeout while (e.g.) Unity loads assets. /// /// Your game logic still has to call Service regularly, or else incoming messages are not dispatched. /// As this typically triggers UI updates, it's easier to call Service from the main/UI thread. /// public bool UseBackgroundWorkerForSending { get; set; } /// Exposes the TransportProtocol of the used PhotonPeer. Settable while not connected. public ConnectionProtocol TransportProtocol { get { return this.chatPeer.TransportProtocol; } set { if (this.chatPeer == null || this.chatPeer.PeerState != PeerStateValue.Disconnected) { this.listener.DebugReturn(DebugLevel.WARNING, "Can't set TransportProtocol. Disconnect first! " + ((this.chatPeer != null) ? "PeerState: " + this.chatPeer.PeerState : "The chatPeer is null.")); return; } this.chatPeer.TransportProtocol = value; } } /// Defines which IPhotonSocket class to use per ConnectionProtocol. /// /// Several platforms have special Socket implementations and slightly different APIs. /// To accomodate this, switching the socket implementation for a network protocol was made available. /// By default, UDP and TCP have socket implementations assigned. /// /// You only need to set the SocketImplementationConfig once, after creating a PhotonPeer /// and before connecting. If you switch the TransportProtocol, the correct implementation is being used. /// public Dictionary SocketImplementationConfig { get { return this.chatPeer.SocketImplementationConfig; } } /// /// Chat client constructor. /// /// The chat listener implementation. /// Connection protocol to be used by this client. Default is . public ChatClient(IChatClientListener listener, ConnectionProtocol protocol = ConnectionProtocol.Udp) { this.listener = listener; this.State = ChatState.Uninitialized; this.chatPeer = new ChatPeer(this, protocol); this.chatPeer.SerializationProtocolType = SerializationProtocol.GpBinaryV18; this.PublicChannels = new Dictionary(); this.PrivateChannels = new Dictionary(); this.PublicChannelsUnsubscribing = new HashSet(); } public bool ConnectUsingSettings(ChatAppSettings appSettings) { if (appSettings == null) { this.listener.DebugReturn(DebugLevel.ERROR, "ConnectUsingSettings failed. The appSettings can't be null.'"); return false; } if (!string.IsNullOrEmpty(appSettings.FixedRegion)) { this.ChatRegion = appSettings.FixedRegion; } this.DebugOut = appSettings.NetworkLogging; this.TransportProtocol = appSettings.Protocol; this.EnableProtocolFallback = appSettings.EnableProtocolFallback; if (!appSettings.IsDefaultNameServer) { this.chatPeer.NameServerHost = appSettings.Server; this.chatPeer.NameServerPortOverride = appSettings.Port; } return this.Connect(appSettings.AppIdChat, appSettings.AppVersion, this.AuthValues); } /// /// Connects this client to the Photon Chat Cloud service, which will also authenticate the user (and set a UserId). /// /// Get your Photon Chat AppId from the Dashboard. /// Any version string you make up. Used to separate users and variants of your clients, which might be incompatible. /// Values for authentication. You can leave this null, if you set a UserId before. If you set authValues, they will override any UserId set before. /// public bool Connect(string appId, string appVersion, AuthenticationValues authValues) { this.chatPeer.TimePingInterval = 3000; this.DisconnectedCause = ChatDisconnectCause.None; if (authValues != null) { this.AuthValues = authValues; } this.AppId = appId; this.AppVersion = appVersion; this.didAuthenticate = false; this.chatPeer.QuickResendAttempts = 2; this.chatPeer.SentCountAllowance = 7; // clean all channels this.PublicChannels.Clear(); this.PrivateChannels.Clear(); this.PublicChannelsUnsubscribing.Clear(); #if UNITY_WEBGL if (this.TransportProtocol == ConnectionProtocol.Tcp || this.TransportProtocol == ConnectionProtocol.Udp) { this.listener.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure."); this.TransportProtocol = ConnectionProtocol.WebSocketSecure; } #endif this.NameServerAddress = this.chatPeer.NameServerAddress; bool isConnecting = this.chatPeer.Connect(); if (isConnecting) { this.State = ChatState.ConnectingToNameServer; } if (this.UseBackgroundWorkerForSending) { #if UNITY_SWITCH SupportClass.StartBackgroundCalls(this.SendOutgoingInBackground, this.msDeltaForServiceCalls); // as workaround, we don't name the Thread. #else SupportClass.StartBackgroundCalls(this.SendOutgoingInBackground, this.msDeltaForServiceCalls, "ChatClient Service Thread"); #endif } return isConnecting; } /// /// Connects this client to the Photon Chat Cloud service, which will also authenticate the user (and set a UserId). /// This also sets an online status once connected. By default it will set user status to . /// See for more information. /// /// Get your Photon Chat AppId from the Dashboard. /// Any version string you make up. Used to separate users and variants of your clients, which might be incompatible. /// Values for authentication. You can leave this null, if you set a UserId before. If you set authValues, they will override any UserId set before. /// User status to set when connected. Predefined states are in class . Other values can be used at will. /// Optional status Also sets a status-message which your friends can get. /// If the connection attempt could be sent at all. public bool ConnectAndSetStatus(string appId, string appVersion, AuthenticationValues authValues, int status = ChatUserStatus.Online, object message = null) { statusToSetWhenConnected = status; messageToSetWhenConnected = message; return Connect(appId, appVersion, authValues); } /// /// Must be called regularly to keep connection between client and server alive and to process incoming messages. /// /// /// This method limits the effort it does automatically using the private variable msDeltaForServiceCalls. /// That value is lower for connect and multiplied by 4 when chat-server connection is ready. /// public void Service() { // Dispatch until every already-received message got dispatched while (this.HasPeer && this.chatPeer.DispatchIncomingCommands()) { } // if there is no background thread for sending, Service() will do that as well, in intervals if (!this.UseBackgroundWorkerForSending) { if (Environment.TickCount - this.msTimestampOfLastServiceCall > this.msDeltaForServiceCalls || this.msTimestampOfLastServiceCall == 0) { this.msTimestampOfLastServiceCall = Environment.TickCount; while (this.HasPeer && this.chatPeer.SendOutgoingCommands()) { } } } } /// /// Called by a separate thread, this sends outgoing commands of this peer, as long as it's connected. /// /// True as long as the client is not disconnected. private bool SendOutgoingInBackground() { while (this.HasPeer && this.chatPeer.SendOutgoingCommands()) { } return this.State != ChatState.Disconnected; } /// Obsolete: Better use UseBackgroundWorkerForSending and Service(). [Obsolete("Better use UseBackgroundWorkerForSending and Service().")] public void SendAcksOnly() { if (this.HasPeer) this.chatPeer.SendAcksOnly(); } /// /// Disconnects from the Chat Server by sending a "disconnect command", which prevents a timeout server-side. /// public void Disconnect(ChatDisconnectCause cause = ChatDisconnectCause.DisconnectByClientLogic) { if (this.HasPeer && this.chatPeer.PeerState != PeerStateValue.Disconnected) { this.State = ChatState.Disconnecting; this.DisconnectedCause = cause; this.chatPeer.Disconnect(); } } /// /// Locally shuts down the connection to the Chat Server. This resets states locally but the server will have to timeout this peer. /// public void StopThread() { if (this.HasPeer) { this.chatPeer.StopThread(); } } /// Sends operation to subscribe to a list of channels by name. /// List of channels to subscribe to. Avoid null or empty values. /// If the operation could be sent at all (Example: Fails if not connected to Chat Server). public bool Subscribe(string[] channels) { return this.Subscribe(channels, 0); } /// /// Sends operation to subscribe to a list of channels by name and possibly retrieve messages we did not receive while unsubscribed. /// /// List of channels to subscribe to. Avoid null or empty values. /// ID of last message received per channel. Useful when re subscribing to receive only messages we missed. /// If the operation could be sent at all (Example: Fails if not connected to Chat Server). public bool Subscribe(string[] channels, int[] lastMsgIds) { if (!this.CanChat) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "Subscribe called while not connected to front end server."); } return false; } if (channels == null || channels.Length == 0) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "Subscribe can't be called for empty or null channels-list."); } return false; } for (int i = 0; i < channels.Length; i++) { if (string.IsNullOrEmpty(channels[i])) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Subscribe can't be called with a null or empty channel name at index {0}.", i)); } return false; } } if (lastMsgIds == null || lastMsgIds.Length != channels.Length) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "Subscribe can't be called when \"lastMsgIds\" array is null or does not have the same length as \"channels\" array."); } return false; } Dictionary opParameters = new Dictionary { { ChatParameterCode.Channels, channels }, { ChatParameterCode.MsgIds, lastMsgIds}, { ChatParameterCode.HistoryLength, -1 } // server will decide how many messages to send to client }; return this.chatPeer.SendOperation(ChatOperationCode.Subscribe, opParameters, SendOptions.SendReliable); } /// /// Sends operation to subscribe client to channels, optionally fetching a number of messages from the cache. /// /// /// Subscribes channels will forward new messages to this user. Use PublishMessage to do so. /// The messages cache is limited but can be useful to get into ongoing conversations, if that's needed. /// /// List of channels to subscribe to. Avoid null or empty values. /// 0: no history. 1 and higher: number of messages in history. -1: all available history. /// If the operation could be sent at all (Example: Fails if not connected to Chat Server). public bool Subscribe(string[] channels, int messagesFromHistory) { if (!this.CanChat) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "Subscribe called while not connected to front end server."); } return false; } if (channels == null || channels.Length == 0) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "Subscribe can't be called for empty or null channels-list."); } return false; } return this.SendChannelOperation(channels, (byte)ChatOperationCode.Subscribe, messagesFromHistory); } /// Unsubscribes from a list of channels, which stops getting messages from those. /// /// The client will remove these channels from the PublicChannels dictionary once the server sent a response to this request. /// /// The request will be sent to the server and IChatClientListener.OnUnsubscribed gets called when the server /// actually removed the channel subscriptions. /// /// Unsubscribe will fail if you include null or empty channel names. /// /// Names of channels to unsubscribe. /// False, if not connected to a chat server. public bool Unsubscribe(string[] channels) { if (!this.CanChat) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "Unsubscribe called while not connected to front end server."); } return false; } if (channels == null || channels.Length == 0) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "Unsubscribe can't be called for empty or null channels-list."); } return false; } foreach (string ch in channels) { this.PublicChannelsUnsubscribing.Add(ch); } return this.SendChannelOperation(channels, ChatOperationCode.Unsubscribe, 0); } /// Sends a message to a public channel which this client subscribed to. /// /// Before you publish to a channel, you have to subscribe it. /// Everyone in that channel will get the message. /// /// Name of the channel to publish to. /// Your message (string or any serializable data). /// Optionally, public messages can be forwarded as webhooks. Configure webhooks for your Chat app to use this. /// False if the client is not yet ready to send messages. public bool PublishMessage(string channelName, object message, bool forwardAsWebhook = false) { return this.publishMessage(channelName, message, true, forwardAsWebhook); } internal bool PublishMessageUnreliable(string channelName, object message, bool forwardAsWebhook = false) { return this.publishMessage(channelName, message, false, forwardAsWebhook); } private bool publishMessage(string channelName, object message, bool reliable, bool forwardAsWebhook = false) { if (!this.CanChat) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "PublishMessage called while not connected to front end server."); } return false; } if (string.IsNullOrEmpty(channelName) || message == null) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "PublishMessage parameters must be non-null and not empty."); } return false; } Dictionary parameters = new Dictionary { { (byte)ChatParameterCode.Channel, channelName }, { (byte)ChatParameterCode.Message, message } }; if (forwardAsWebhook) { parameters.Add(ChatParameterCode.WebFlags, (byte)0x1); } return this.chatPeer.SendOperation(ChatOperationCode.Publish, parameters, new SendOptions() { Reliability = reliable }); } /// /// Sends a private message to a single target user. Calls OnPrivateMessage on the receiving client. /// /// Username to send this message to. /// The message you want to send. Can be a simple string or anything serializable. /// Optionally, private messages can be forwarded as webhooks. Configure webhooks for your Chat app to use this. /// True if this clients can send the message to the server. public bool SendPrivateMessage(string target, object message, bool forwardAsWebhook = false) { return this.SendPrivateMessage(target, message, false, forwardAsWebhook); } /// /// Sends a private message to a single target user. Calls OnPrivateMessage on the receiving client. /// /// Username to send this message to. /// The message you want to send. Can be a simple string or anything serializable. /// Optionally, private messages can be encrypted. Encryption is not end-to-end as the server decrypts the message. /// Optionally, private messages can be forwarded as webhooks. Configure webhooks for your Chat app to use this. /// True if this clients can send the message to the server. public bool SendPrivateMessage(string target, object message, bool encrypt, bool forwardAsWebhook) { return this.sendPrivateMessage(target, message, encrypt, true, forwardAsWebhook); } internal bool SendPrivateMessageUnreliable(string target, object message, bool encrypt, bool forwardAsWebhook = false) { return this.sendPrivateMessage(target, message, encrypt, false, forwardAsWebhook); } private bool sendPrivateMessage(string target, object message, bool encrypt, bool reliable, bool forwardAsWebhook = false) { if (!this.CanChat) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "SendPrivateMessage called while not connected to front end server."); } return false; } if (string.IsNullOrEmpty(target) || message == null) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "SendPrivateMessage parameters must be non-null and not empty."); } return false; } Dictionary parameters = new Dictionary { { ChatParameterCode.UserId, target }, { ChatParameterCode.Message, message } }; if (forwardAsWebhook) { parameters.Add(ChatParameterCode.WebFlags, (byte)0x1); } return this.chatPeer.SendOperation(ChatOperationCode.SendPrivate, parameters, new SendOptions() { Reliability = reliable, Encrypt = encrypt }); } /// Sets the user's status (pre-defined or custom) and an optional message. /// /// The predefined status values can be found in class ChatUserStatus. /// State ChatUserStatus.Invisible will make you offline for everyone and send no message. /// /// You can set custom values in the status integer. Aside from the pre-configured ones, /// all states will be considered visible and online. Else, no one would see the custom state. /// /// The message object can be anything that Photon can serialize, including (but not limited to) /// Hashtable, object[] and string. This value is defined by your own conventions. /// /// Predefined states are in class ChatUserStatus. Other values can be used at will. /// Optional string message or null. /// If true, the message gets ignored. It can be null but won't replace any current message. /// True if the operation gets called on the server. private bool SetOnlineStatus(int status, object message, bool skipMessage) { if (!this.CanChat) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "SetOnlineStatus called while not connected to front end server."); } return false; } Dictionary parameters = new Dictionary { { ChatParameterCode.Status, status }, }; if (skipMessage) { parameters[ChatParameterCode.SkipMessage] = true; } else { parameters[ChatParameterCode.Message] = message; } return this.chatPeer.SendOperation(ChatOperationCode.UpdateStatus, parameters, SendOptions.SendReliable); } /// Sets the user's status without changing your status-message. /// /// The predefined status values can be found in class ChatUserStatus. /// State ChatUserStatus.Invisible will make you offline for everyone and send no message. /// /// You can set custom values in the status integer. Aside from the pre-configured ones, /// all states will be considered visible and online. Else, no one would see the custom state. /// /// This overload does not change the set message. /// /// Predefined states are in class ChatUserStatus. Other values can be used at will. /// True if the operation gets called on the server. public bool SetOnlineStatus(int status) { return this.SetOnlineStatus(status, null, true); } /// Sets the user's status without changing your status-message. /// /// The predefined status values can be found in class ChatUserStatus. /// State ChatUserStatus.Invisible will make you offline for everyone and send no message. /// /// You can set custom values in the status integer. Aside from the pre-configured ones, /// all states will be considered visible and online. Else, no one would see the custom state. /// /// The message object can be anything that Photon can serialize, including (but not limited to) /// Hashtable, object[] and string. This value is defined by your own conventions. /// /// Predefined states are in class ChatUserStatus. Other values can be used at will. /// Also sets a status-message which your friends can get. /// True if the operation gets called on the server. public bool SetOnlineStatus(int status, object message) { return this.SetOnlineStatus(status, message, false); } /// /// Adds friends to a list on the Chat Server which will send you status updates for those. /// /// /// AddFriends and RemoveFriends enable clients to handle their friend list /// in the Photon Chat server. Having users on your friends list gives you access /// to their current online status (and whatever info your client sets in it). /// /// Each user can set an online status consisting of an integer and an arbitrary /// (serializable) object. The object can be null, Hashtable, object[] or anything /// else Photon can serialize. /// /// The status is published automatically to friends (anyone who set your user ID /// with AddFriends). /// /// Photon flushes friends-list when a chat client disconnects, so it has to be /// set each time. If your community API gives you access to online status already, /// you could filter and set online friends in AddFriends. /// /// Actual friend relations are not persistent and have to be stored outside /// of Photon. /// /// Array of friend userIds. /// If the operation could be sent. public bool AddFriends(string[] friends) { if (!this.CanChat) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "AddFriends called while not connected to front end server."); } return false; } if (friends == null || friends.Length == 0) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "AddFriends can't be called for empty or null list."); } return false; } if (friends.Length > FriendRequestListMax) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "AddFriends max list size exceeded: " + friends.Length + " > " + FriendRequestListMax); } return false; } Dictionary parameters = new Dictionary { { ChatParameterCode.Friends, friends }, }; return this.chatPeer.SendOperation(ChatOperationCode.AddFriends, parameters, SendOptions.SendReliable); } /// /// Removes the provided entries from the list on the Chat Server and stops their status updates. /// /// /// Photon flushes friends-list when a chat client disconnects. Unless you want to /// remove individual entries, you don't have to RemoveFriends. /// /// AddFriends and RemoveFriends enable clients to handle their friend list /// in the Photon Chat server. Having users on your friends list gives you access /// to their current online status (and whatever info your client sets in it). /// /// Each user can set an online status consisting of an integer and an arbitratry /// (serializable) object. The object can be null, Hashtable, object[] or anything /// else Photon can serialize. /// /// The status is published automatically to friends (anyone who set your user ID /// with AddFriends). /// /// Photon flushes friends-list when a chat client disconnects, so it has to be /// set each time. If your community API gives you access to online status already, /// you could filter and set online friends in AddFriends. /// /// Actual friend relations are not persistent and have to be stored outside /// of Photon. /// /// AddFriends and RemoveFriends enable clients to handle their friend list /// in the Photon Chat server. Having users on your friends list gives you access /// to their current online status (and whatever info your client sets in it). /// /// Each user can set an online status consisting of an integer and an arbitratry /// (serializable) object. The object can be null, Hashtable, object[] or anything /// else Photon can serialize. /// /// The status is published automatically to friends (anyone who set your user ID /// with AddFriends). /// /// /// Actual friend relations are not persistent and have to be stored outside /// of Photon. /// /// Array of friend userIds. /// If the operation could be sent. public bool RemoveFriends(string[] friends) { if (!this.CanChat) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "RemoveFriends called while not connected to front end server."); } return false; } if (friends == null || friends.Length == 0) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "RemoveFriends can't be called for empty or null list."); } return false; } if (friends.Length > FriendRequestListMax) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "RemoveFriends max list size exceeded: " + friends.Length + " > " + FriendRequestListMax); } return false; } Dictionary parameters = new Dictionary { { ChatParameterCode.Friends, friends }, }; return this.chatPeer.SendOperation(ChatOperationCode.RemoveFriends, parameters, SendOptions.SendReliable); } /// /// Get you the (locally used) channel name for the chat between this client and another user. /// /// Remote user's name or UserId. /// The (locally used) channel name for a private channel. /// Do not subscribe to this channel. /// Private channels do not need to be explicitly subscribed to. /// Use this for debugging purposes mainly. public string GetPrivateChannelNameByUser(string userName) { return string.Format("{0}:{1}", this.UserId, userName); } /// /// Simplified access to either private or public channels by name. /// /// Name of the channel to get. For private channels, the channel-name is composed of both user's names. /// Define if you expect a private or public channel. /// Out parameter gives you the found channel, if any. /// True if the channel was found. /// Public channels exist only when subscribed to them. /// Private channels exist only when at least one message is exchanged with the target user privately. public bool TryGetChannel(string channelName, bool isPrivate, out ChatChannel channel) { if (!isPrivate) { return this.PublicChannels.TryGetValue(channelName, out channel); } else { return this.PrivateChannels.TryGetValue(channelName, out channel); } } /// /// Simplified access to all channels by name. Checks public channels first, then private ones. /// /// Name of the channel to get. /// Out parameter gives you the found channel, if any. /// True if the channel was found. /// Public channels exist only when subscribed to them. /// Private channels exist only when at least one message is exchanged with the target user privately. public bool TryGetChannel(string channelName, out ChatChannel channel) { bool found = false; found = this.PublicChannels.TryGetValue(channelName, out channel); if (found) return true; found = this.PrivateChannels.TryGetValue(channelName, out channel); return found; } /// /// Simplified access to private channels by target user. /// /// UserId of the target user in the private channel. /// Out parameter gives you the found channel, if any. /// True if the channel was found. public bool TryGetPrivateChannelByUser(string userId, out ChatChannel channel) { channel = null; if (string.IsNullOrEmpty(userId)) { return false; } string channelName = this.GetPrivateChannelNameByUser(userId); return this.TryGetChannel(channelName, true, out channel); } /// /// Sets the level (and amount) of debug output provided by the library. /// /// /// This affects the callbacks to IChatClientListener.DebugReturn. /// Default Level: Error. /// public DebugLevel DebugOut { set { this.chatPeer.DebugOut = value; } get { return this.chatPeer.DebugOut; } } #region Private methods area #region IPhotonPeerListener implementation void IPhotonPeerListener.DebugReturn(DebugLevel level, string message) { this.listener.DebugReturn(level, message); } void IPhotonPeerListener.OnEvent(EventData eventData) { switch (eventData.Code) { case ChatEventCode.ChatMessages: this.HandleChatMessagesEvent(eventData); break; case ChatEventCode.PrivateMessage: this.HandlePrivateMessageEvent(eventData); break; case ChatEventCode.StatusUpdate: this.HandleStatusUpdate(eventData); break; case ChatEventCode.Subscribe: this.HandleSubscribeEvent(eventData); break; case ChatEventCode.Unsubscribe: this.HandleUnsubscribeEvent(eventData); break; case ChatEventCode.UserSubscribed: this.HandleUserSubscribedEvent(eventData); break; case ChatEventCode.UserUnsubscribed: this.HandleUserUnsubscribedEvent(eventData); break; #if CHAT_EXTENDED case ChatEventCode.PropertiesChanged: this.HandlePropertiesChanged(eventData); break; case ChatEventCode.ErrorInfo: this.HandleErrorInfoEvent(eventData); break; #endif } } void IPhotonPeerListener.OnOperationResponse(OperationResponse operationResponse) { switch (operationResponse.OperationCode) { case (byte)ChatOperationCode.Authenticate: this.HandleAuthResponse(operationResponse); break; // the following operations usually don't return useful data and no error. case (byte)ChatOperationCode.Subscribe: case (byte)ChatOperationCode.Unsubscribe: case (byte)ChatOperationCode.Publish: case (byte)ChatOperationCode.SendPrivate: default: if ((operationResponse.ReturnCode != 0) && (this.DebugOut >= DebugLevel.ERROR)) { if (operationResponse.ReturnCode == -2) { this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Chat Operation {0} unknown on server. Check your AppId and make sure it's for a Chat application.", operationResponse.OperationCode)); } else { this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Chat Operation {0} failed (Code: {1}). Debug Message: {2}", operationResponse.OperationCode, operationResponse.ReturnCode, operationResponse.DebugMessage)); } } break; } } void IPhotonPeerListener.OnStatusChanged(StatusCode statusCode) { switch (statusCode) { case StatusCode.Connect: if (!this.chatPeer.IsProtocolSecure) { if (!this.chatPeer.EstablishEncryption()) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "Error establishing encryption"); } } } else { this.TryAuthenticateOnNameServer(); } if (this.State == ChatState.ConnectingToNameServer) { this.State = ChatState.ConnectedToNameServer; this.listener.OnChatStateChange(this.State); } else if (this.State == ChatState.ConnectingToFrontEnd) { if (!this.AuthenticateOnFrontEnd()) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Error authenticating on frontend! Check log output, AuthValues and if you're connected. State: {0}", this.State)); } } } break; case StatusCode.EncryptionEstablished: // once encryption is available, the client should send one (secure) authenticate. it includes the AppId (which identifies your app on the Photon Cloud) this.TryAuthenticateOnNameServer(); break; case StatusCode.Disconnect: switch (this.State) { case ChatState.ConnectWithFallbackProtocol: this.EnableProtocolFallback = false; // the client does a fallback only one time this.chatPeer.NameServerPortOverride = 0; // resets a value in the peer only (as we change the protocol, the port has to change, too) this.chatPeer.TransportProtocol = (this.chatPeer.TransportProtocol == ConnectionProtocol.Tcp) ? ConnectionProtocol.Udp : ConnectionProtocol.Tcp; this.Connect(this.AppId, this.AppVersion, null); // the client now has to return, instead of break, to avoid further processing of the disconnect call return; case ChatState.Authenticated: this.ConnectToFrontEnd(); // client disconnected from nameserver after authentication // to switch to frontend return; case ChatState.Disconnecting: // expected disconnect break; default: // unexpected disconnect, we log warning and stacktrace string stacktrace = string.Empty; #if DEBUG && !NETFX_CORE stacktrace = new System.Diagnostics.StackTrace(true).ToString(); #endif this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Got a unexpected Disconnect in ChatState: {0}. Server: {1} Trace: {2}", this.State, this.chatPeer.ServerAddress, stacktrace)); break; } if (this.AuthValues != null) { this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) } this.State = ChatState.Disconnected; this.listener.OnChatStateChange(ChatState.Disconnected); this.listener.OnDisconnected(); break; case StatusCode.DisconnectByServerUserLimit: this.listener.DebugReturn(DebugLevel.ERROR, "This connection was rejected due to the apps CCU limit."); this.Disconnect(ChatDisconnectCause.MaxCcuReached); break; case StatusCode.ExceptionOnConnect: case StatusCode.SecurityExceptionOnConnect: case StatusCode.EncryptionFailedToEstablish: this.DisconnectedCause = ChatDisconnectCause.ExceptionOnConnect; // if enabled, the client can attempt to connect with another networking-protocol to check if that connects if (this.EnableProtocolFallback && this.State == ChatState.ConnectingToNameServer) { this.State = ChatState.ConnectWithFallbackProtocol; } else { this.Disconnect(ChatDisconnectCause.ExceptionOnConnect); } break; case StatusCode.Exception: case StatusCode.ExceptionOnReceive: this.Disconnect(ChatDisconnectCause.Exception); break; case StatusCode.DisconnectByServerTimeout: this.Disconnect(ChatDisconnectCause.ServerTimeout); break; case StatusCode.DisconnectByServerLogic: this.Disconnect(ChatDisconnectCause.DisconnectByServerLogic); break; case StatusCode.DisconnectByServerReasonUnknown: this.Disconnect(ChatDisconnectCause.DisconnectByServerReasonUnknown); break; case StatusCode.TimeoutDisconnect: this.DisconnectedCause = ChatDisconnectCause.ClientTimeout; // if enabled, the client can attempt to connect with another networking-protocol to check if that connects if (this.EnableProtocolFallback && this.State == ChatState.ConnectingToNameServer) { this.State = ChatState.ConnectWithFallbackProtocol; } else { this.Disconnect(ChatDisconnectCause.ClientTimeout); } break; } } #if SDK_V4 void IPhotonPeerListener.OnMessage(object msg) { string channelName = null; var receivedBytes = (byte[])msg; var channelId = BitConverter.ToInt32(receivedBytes, 0); var messageBytes = new byte[receivedBytes.Length - 4]; Array.Copy(receivedBytes, 4, messageBytes, 0, receivedBytes.Length - 4); foreach (var channel in this.PublicChannels) { if (channel.Value.ChannelID == channelId) { channelName = channel.Key; break; } } if (channelName != null) { this.listener.DebugReturn(DebugLevel.ALL, string.Format("got OnMessage in channel {0}", channelName)); } else { this.listener.DebugReturn(DebugLevel.WARNING, string.Format("got OnMessage in unknown channel {0}", channelId)); } this.listener.OnReceiveBroadcastMessage(channelName, messageBytes); } #endif #endregion private void TryAuthenticateOnNameServer() { if (!this.didAuthenticate) { this.didAuthenticate = this.chatPeer.AuthenticateOnNameServer(this.AppId, this.AppVersion, this.ChatRegion, this.AuthValues); if (!this.didAuthenticate) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Error calling OpAuthenticate! Did not work on NameServer. Check log output, AuthValues and if you're connected. State: {0}", this.State)); } } } } private bool SendChannelOperation(string[] channels, byte operation, int historyLength) { Dictionary opParameters = new Dictionary { { (byte)ChatParameterCode.Channels, channels } }; if (historyLength != 0) { opParameters.Add((byte)ChatParameterCode.HistoryLength, historyLength); } return this.chatPeer.SendOperation(operation, opParameters, SendOptions.SendReliable); } private void HandlePrivateMessageEvent(EventData eventData) { //Console.WriteLine(SupportClass.DictionaryToString(eventData.Parameters)); object message = (object)eventData.Parameters[(byte)ChatParameterCode.Message]; string sender = (string)eventData.Parameters[(byte)ChatParameterCode.Sender]; int msgId = (int)eventData.Parameters[ChatParameterCode.MsgId]; string channelName; if (this.UserId != null && this.UserId.Equals(sender)) { string target = (string)eventData.Parameters[(byte)ChatParameterCode.UserId]; channelName = this.GetPrivateChannelNameByUser(target); } else { channelName = this.GetPrivateChannelNameByUser(sender); } ChatChannel channel; if (!this.PrivateChannels.TryGetValue(channelName, out channel)) { channel = new ChatChannel(channelName); channel.IsPrivate = true; channel.MessageLimit = this.MessageLimit; this.PrivateChannels.Add(channel.Name, channel); } channel.Add(sender, message, msgId); this.listener.OnPrivateMessage(sender, message, channelName); } private void HandleChatMessagesEvent(EventData eventData) { object[] messages = (object[])eventData.Parameters[(byte)ChatParameterCode.Messages]; string[] senders = (string[])eventData.Parameters[(byte)ChatParameterCode.Senders]; string channelName = (string)eventData.Parameters[(byte)ChatParameterCode.Channel]; int lastMsgId = (int)eventData.Parameters[ChatParameterCode.MsgId]; ChatChannel channel; if (!this.PublicChannels.TryGetValue(channelName, out channel)) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "Channel " + channelName + " for incoming message event not found."); } return; } channel.Add(senders, messages, lastMsgId); this.listener.OnGetMessages(channelName, senders, messages); } private void HandleSubscribeEvent(EventData eventData) { string[] channelsInResponse = (string[])eventData.Parameters[ChatParameterCode.Channels]; bool[] results = (bool[])eventData.Parameters[ChatParameterCode.SubscribeResults]; for (int i = 0; i < channelsInResponse.Length; i++) { if (results[i]) { string channelName = channelsInResponse[i]; ChatChannel channel; if (!this.PublicChannels.TryGetValue(channelName, out channel)) { channel = new ChatChannel(channelName); channel.MessageLimit = this.MessageLimit; this.PublicChannels.Add(channel.Name, channel); } object temp; if (eventData.Parameters.TryGetValue(ChatParameterCode.Properties, out temp)) { Dictionary channelProperties = temp as Dictionary; channel.ReadChannelProperties(channelProperties); } if (channel.PublishSubscribers) // or maybe remove check & always add anyway? { channel.Subscribers.Add(this.UserId); } if (eventData.Parameters.TryGetValue(ChatParameterCode.ChannelSubscribers, out temp)) { string[] subscribers = temp as string[]; channel.AddSubscribers(subscribers); } #if CHAT_EXTENDED if (eventData.Parameters.TryGetValue(ChatParameterCode.UserProperties, out temp)) { Dictionary> userProperties = temp as Dictionary>; foreach (var pair in userProperties) { channel.ReadUserProperties(pair.Key, pair.Value); } } #endif } } this.listener.OnSubscribed(channelsInResponse, results); } private void HandleUnsubscribeEvent(EventData eventData) { string[] channelsInRequest = (string[])eventData[ChatParameterCode.Channels]; for (int i = 0; i < channelsInRequest.Length; i++) { string channelName = channelsInRequest[i]; this.PublicChannels.Remove(channelName); this.PublicChannelsUnsubscribing.Remove(channelName); } this.listener.OnUnsubscribed(channelsInRequest); } private void HandleAuthResponse(OperationResponse operationResponse) { if (this.DebugOut >= DebugLevel.INFO) { this.listener.DebugReturn(DebugLevel.INFO, operationResponse.ToStringFull() + " on: " + this.chatPeer.NameServerAddress); } if (operationResponse.ReturnCode == 0) { if (this.State == ChatState.ConnectedToNameServer) { this.State = ChatState.Authenticated; this.listener.OnChatStateChange(this.State); if (operationResponse.Parameters.ContainsKey(ParameterCode.Secret)) { if (this.AuthValues == null) { this.AuthValues = new AuthenticationValues(); } this.AuthValues.Token = operationResponse[ParameterCode.Secret] as string; this.FrontendAddress = (string)operationResponse[ParameterCode.Address]; // we disconnect and status handler starts to connect to front end this.chatPeer.Disconnect(); } else { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "No secret in authentication response."); } } if (operationResponse.Parameters.ContainsKey(ParameterCode.UserId)) { string incomingId = operationResponse.Parameters[ParameterCode.UserId] as string; if (!string.IsNullOrEmpty(incomingId)) { this.UserId = incomingId; this.listener.DebugReturn(DebugLevel.INFO, string.Format("Received your UserID from server. Updating local value to: {0}", this.UserId)); } } } else if (this.State == ChatState.ConnectingToFrontEnd) { this.State = ChatState.ConnectedToFrontEnd; this.listener.OnChatStateChange(this.State); this.listener.OnConnected(); if (statusToSetWhenConnected.HasValue) { SetOnlineStatus(statusToSetWhenConnected.Value, messageToSetWhenConnected); statusToSetWhenConnected = null; } } } else { //this.listener.DebugReturn(DebugLevel.INFO, operationResponse.ToStringFull() + " NS: " + this.NameServerAddress + " FrontEnd: " + this.frontEndAddress); switch (operationResponse.ReturnCode) { case ErrorCode.InvalidAuthentication: this.DisconnectedCause = ChatDisconnectCause.InvalidAuthentication; break; case ErrorCode.CustomAuthenticationFailed: this.DisconnectedCause = ChatDisconnectCause.CustomAuthenticationFailed; break; case ErrorCode.InvalidRegion: this.DisconnectedCause = ChatDisconnectCause.InvalidRegion; break; case ErrorCode.MaxCcuReached: this.DisconnectedCause = ChatDisconnectCause.MaxCcuReached; break; case ErrorCode.OperationNotAllowedInCurrentState: this.DisconnectedCause = ChatDisconnectCause.OperationNotAllowedInCurrentState; break; case ErrorCode.AuthenticationTicketExpired: this.DisconnectedCause = ChatDisconnectCause.AuthenticationTicketExpired; break; } if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, string.Format("{0} ClientState: {1} ServerAddress: {2}", operationResponse.ToStringFull(), this.State, this.chatPeer.ServerAddress)); } this.Disconnect(this.DisconnectedCause); } } private void HandleStatusUpdate(EventData eventData) { string user = (string)eventData.Parameters[ChatParameterCode.Sender]; int status = (int)eventData.Parameters[ChatParameterCode.Status]; object message = null; bool gotMessage = eventData.Parameters.ContainsKey(ChatParameterCode.Message); if (gotMessage) { message = eventData.Parameters[ChatParameterCode.Message]; } this.listener.OnStatusUpdate(user, status, gotMessage, message); } private bool ConnectToFrontEnd() { this.State = ChatState.ConnectingToFrontEnd; if (this.DebugOut >= DebugLevel.INFO) { this.listener.DebugReturn(DebugLevel.INFO, "Connecting to frontend " + this.FrontendAddress); } #if UNITY_WEBGL if (this.TransportProtocol == ConnectionProtocol.Tcp || this.TransportProtocol == ConnectionProtocol.Udp) { this.listener.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure."); this.TransportProtocol = ConnectionProtocol.WebSocketSecure; } #endif if (!this.chatPeer.Connect(this.FrontendAddress, ChatAppName)) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Connecting to frontend {0} failed.", this.FrontendAddress)); } return false; } return true; } private bool AuthenticateOnFrontEnd() { if (this.AuthValues != null) { if (this.AuthValues.Token == null) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "Can't authenticate on front end server. Secret (AuthValues.Token) is not set"); } return false; } else { Dictionary opParameters = new Dictionary { { (byte)ChatParameterCode.Secret, this.AuthValues.Token } }; if (this.PrivateChatHistoryLength > -1) { opParameters[(byte)ChatParameterCode.HistoryLength] = this.PrivateChatHistoryLength; } return this.chatPeer.SendOperation(ChatOperationCode.Authenticate, opParameters, SendOptions.SendReliable); } } else { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "Can't authenticate on front end server. Authentication Values are not set"); } return false; } } private void HandleUserUnsubscribedEvent(EventData eventData) { string channelName = eventData.Parameters[ChatParameterCode.Channel] as string; string userId = eventData.Parameters[ChatParameterCode.UserId] as string; ChatChannel channel; if (this.PublicChannels.TryGetValue(channelName, out channel)) { if (!channel.PublishSubscribers) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" for incoming UserUnsubscribed (\"{1}\") event does not have PublishSubscribers enabled.", channelName, userId)); } } if (!channel.Subscribers.Remove(userId)) // user not found! { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" does not contain unsubscribed user \"{1}\".", channelName, userId)); } } } else { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" not found for incoming UserUnsubscribed (\"{1}\") event.", channelName, userId)); } } this.listener.OnUserUnsubscribed(channelName, userId); } private void HandleUserSubscribedEvent(EventData eventData) { //TODO: Handle user properties! string channelName = eventData.Parameters[ChatParameterCode.Channel] as string; string userId = eventData.Parameters[ChatParameterCode.UserId] as string; ChatChannel channel; if (this.PublicChannels.TryGetValue(channelName, out channel)) { if (!channel.PublishSubscribers) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" for incoming UserSubscribed (\"{1}\") event does not have PublishSubscribers enabled.", channelName, userId)); } } if (!channel.Subscribers.Add(userId)) // user came back from the dead ? { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" already contains newly subscribed user \"{1}\".", channelName, userId)); } } else if (channel.MaxSubscribers > 0 && channel.Subscribers.Count > channel.MaxSubscribers) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\"'s MaxSubscribers exceeded. count={1} > MaxSubscribers={2}.", channelName, channel.Subscribers.Count, channel.MaxSubscribers)); } } } else { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" not found for incoming UserSubscribed (\"{1}\") event.", channelName, userId)); } } this.listener.OnUserSubscribed(channelName, userId); } #endregion /// /// Subscribe to a single channel and optionally sets its well-know channel properties in case the channel is created. /// /// name of the channel to subscribe to /// ID of the last received message from this channel when re subscribing to receive only missed messages, default is 0 /// how many missed messages to receive from history, default is -1 (available history). 0 will get you no items. Positive values are capped by a server side limit. /// options to be used in case the channel to subscribe to will be created. /// public bool Subscribe(string channel, int lastMsgId = 0, int messagesFromHistory = -1, ChannelCreationOptions creationOptions = null) { if (creationOptions == null) { creationOptions = ChannelCreationOptions.Default; } int maxSubscribers = creationOptions.MaxSubscribers; bool publishSubscribers = creationOptions.PublishSubscribers; if (maxSubscribers < 0) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "Cannot set MaxSubscribers < 0."); } return false; } if (lastMsgId < 0) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, "lastMsgId cannot be < 0."); } return false; } if (messagesFromHistory < -1) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "messagesFromHistory < -1, setting it to -1"); } messagesFromHistory = -1; } if (lastMsgId > 0 && messagesFromHistory == 0) { if (this.DebugOut >= DebugLevel.WARNING) { this.listener.DebugReturn(DebugLevel.WARNING, "lastMsgId will be ignored because messagesFromHistory == 0"); } lastMsgId = 0; } Dictionary properties = null; if (publishSubscribers) { if (maxSubscribers > DefaultMaxSubscribers) { if (this.DebugOut >= DebugLevel.ERROR) { this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Cannot set MaxSubscribers > {0} when PublishSubscribers == true.", DefaultMaxSubscribers)); } return false; } properties = new Dictionary(); properties[ChannelWellKnownProperties.PublishSubscribers] = true; } if (maxSubscribers > 0) { if (properties == null) { properties = new Dictionary(); } properties[ChannelWellKnownProperties.MaxSubscribers] = maxSubscribers; } #if CHAT_EXTENDED if (creationOptions.CustomProperties != null && creationOptions.CustomProperties.Count > 0) { foreach (var pair in creationOptions.CustomProperties) { properties.Add(pair.Key, pair.Value); } } #endif Dictionary opParameters = new Dictionary { { ChatParameterCode.Channels, new[] { channel } } }; if (messagesFromHistory != 0) { opParameters.Add(ChatParameterCode.HistoryLength, messagesFromHistory); } if (lastMsgId > 0) { opParameters.Add(ChatParameterCode.MsgIds, new[] { lastMsgId }); } if (properties != null && properties.Count > 0) { opParameters.Add(ChatParameterCode.Properties, properties); } return this.chatPeer.SendOperation(ChatOperationCode.Subscribe, opParameters, SendOptions.SendReliable); } #if CHAT_EXTENDED internal bool SetChannelProperties(string channelName, Dictionary channelProperties, Dictionary expectedProperties = null, bool httpForward = false) { if (!this.CanChat) { this.listener.DebugReturn(DebugLevel.ERROR, "SetChannelProperties called while not connected to front end server."); return false; } if (string.IsNullOrEmpty(channelName) || channelProperties == null || channelProperties.Count == 0) { this.listener.DebugReturn(DebugLevel.WARNING, "SetChannelProperties parameters must be non-null and not empty."); return false; } Dictionary parameters = new Dictionary { { ChatParameterCode.Channel, channelName }, { ChatParameterCode.Properties, channelProperties }, { ChatParameterCode.Broadcast, true } }; if (httpForward) { parameters.Add(ChatParameterCode.WebFlags, HttpForwardWebFlag); } if (expectedProperties != null && expectedProperties.Count > 0) { parameters.Add(ChatParameterCode.ExpectedValues, expectedProperties); } return this.chatPeer.SendOperation(ChatOperationCode.SetProperties, parameters, SendOptions.SendReliable); } public bool SetCustomChannelProperties(string channelName, Dictionary channelProperties, Dictionary expectedProperties = null, bool httpForward = false) { if (channelProperties != null && channelProperties.Count > 0) { Dictionary properties = new Dictionary(channelProperties.Count); foreach (var pair in channelProperties) { properties.Add(pair.Key, pair.Value); } Dictionary expected = null; if (expectedProperties != null && expectedProperties.Count > 0) { expected = new Dictionary(expectedProperties.Count); foreach (var pair in expectedProperties) { expected.Add(pair.Key, pair.Value); } } return this.SetChannelProperties(channelName, properties, expected, httpForward); } return this.SetChannelProperties(channelName, null); } public bool SetCustomUserProperties(string channelName, string userId, Dictionary userProperties, Dictionary expectedProperties = null, bool httpForward = false) { if (userProperties != null && userProperties.Count > 0) { Dictionary properties = new Dictionary(userProperties.Count); foreach (var pair in userProperties) { properties.Add(pair.Key, pair.Value); } Dictionary expected = null; if (expectedProperties != null && expectedProperties.Count > 0) { expected = new Dictionary(expectedProperties.Count); foreach (var pair in expectedProperties) { expected.Add(pair.Key, pair.Value); } } return this.SetUserProperties(channelName, userId, properties, expected, httpForward); } return this.SetUserProperties(channelName, userId, null); } internal bool SetUserProperties(string channelName, string userId, Dictionary channelProperties, Dictionary expectedProperties = null, bool httpForward = false) { if (!this.CanChat) { this.listener.DebugReturn(DebugLevel.ERROR, "SetUserProperties called while not connected to front end server."); return false; } if (string.IsNullOrEmpty(channelName)) { this.listener.DebugReturn(DebugLevel.WARNING, "SetUserProperties \"channelName\" parameter must be non-null and not empty."); return false; } if (channelProperties == null || channelProperties.Count == 0) { this.listener.DebugReturn(DebugLevel.WARNING, "SetUserProperties \"channelProperties\" parameter must be non-null and not empty."); return false; } if (string.IsNullOrEmpty(userId)) { this.listener.DebugReturn(DebugLevel.WARNING, "SetUserProperties \"userId\" parameter must be non-null and not empty."); return false; } Dictionary parameters = new Dictionary { { ChatParameterCode.Channel, channelName }, { ChatParameterCode.Properties, channelProperties }, { ChatParameterCode.UserId, userId }, { ChatParameterCode.Broadcast, true } }; if (httpForward) { parameters.Add(ChatParameterCode.WebFlags, HttpForwardWebFlag); } if (expectedProperties != null && expectedProperties.Count > 0) { parameters.Add(ChatParameterCode.ExpectedValues, expectedProperties); } return this.chatPeer.SendOperation(ChatOperationCode.SetProperties, parameters, SendOptions.SendReliable); } private void HandlePropertiesChanged(EventData eventData) { string channelName = eventData.Parameters[ChatParameterCode.Channel] as string; ChatChannel channel; if (!this.PublicChannels.TryGetValue(channelName, out channel)) { this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel {0} for incoming ChannelPropertiesUpdated event not found.", channelName)); return; } string senderId = eventData.Parameters[ChatParameterCode.Sender] as string; Dictionary changedProperties = eventData.Parameters[ChatParameterCode.Properties] as Dictionary; object temp; if (eventData.Parameters.TryGetValue(ChatParameterCode.UserId, out temp)) { string targetUserId = temp as string; channel.ReadUserProperties(targetUserId, changedProperties); this.listener.OnUserPropertiesChanged(channelName, targetUserId, senderId, changedProperties); } else { channel.ReadChannelProperties(changedProperties); this.listener.OnChannelPropertiesChanged(channelName, senderId, changedProperties); } } private void HandleErrorInfoEvent(EventData eventData) { string channel = eventData.Parameters[ChatParameterCode.Channel] as string; string msg = eventData.Parameters[ChatParameterCode.DebugMessage] as string; object data = eventData.Parameters[ChatParameterCode.DebugData]; this.listener.OnErrorInfo(channel, msg, data); } #endif } }