// ----------------------------------------------------------------------------------------------------------------------
// 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