ChatClient.cs 87 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860
  1. // ----------------------------------------------------------------------------------------------------------------------
  2. // <summary>The Photon Chat Api enables clients to connect to a chat server and communicate with other clients.</summary>
  3. // <remarks>ChatClient is the main class of this api.</remarks>
  4. // <copyright company="Exit Games GmbH">Photon Chat Api - Copyright (C) 2014 Exit Games GmbH</copyright>
  5. // ----------------------------------------------------------------------------------------------------------------------
  6. #if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER
  7. #define SUPPORTED_UNITY
  8. #endif
  9. namespace Photon.Chat
  10. {
  11. using System;
  12. using System.Collections.Generic;
  13. using System.Diagnostics;
  14. using ExitGames.Client.Photon;
  15. #if SUPPORTED_UNITY || NETFX_CORE
  16. using Hashtable = ExitGames.Client.Photon.Hashtable;
  17. using SupportClass = ExitGames.Client.Photon.SupportClass;
  18. #endif
  19. /// <summary>Central class of the Photon Chat API to connect, handle channels and messages.</summary>
  20. /// <remarks>
  21. /// This class must be instantiated with a IChatClientListener instance to get the callbacks.
  22. /// Integrate it into your game loop by calling Service regularly. If the target platform supports Threads/Tasks,
  23. /// set UseBackgroundWorkerForSending = true, to let the ChatClient keep the connection by sending from
  24. /// an independent thread.
  25. ///
  26. /// Call Connect with an AppId that is setup as Photon Chat application. Note: Connect covers multiple
  27. /// messages between this client and the servers. A short workflow will connect you to a chat server.
  28. ///
  29. /// Each ChatClient resembles a user in chat (set in Connect). Each user automatically subscribes a channel
  30. /// for incoming private messages and can message any other user privately.
  31. /// Before you publish messages in any non-private channel, that channel must be subscribed.
  32. ///
  33. /// PublicChannels is a list of subscribed channels, containing messages and senders.
  34. /// PrivateChannels contains all incoming and sent private messages.
  35. /// </remarks>
  36. public class ChatClient : IPhotonPeerListener
  37. {
  38. const int FriendRequestListMax = 1024;
  39. /// <summary> Default maximum value possible for <see cref="ChatChannel.MaxSubscribers"/> when <see cref="ChatChannel.PublishSubscribers"/> is enabled</summary>
  40. public const int DefaultMaxSubscribers = 100;
  41. private const byte HttpForwardWebFlag = 0x01;
  42. /// <summary>Enables a fallback to another protocol in case a connect to the Name Server fails.</summary>
  43. /// <remarks>
  44. /// When connecting to the Name Server fails for a first time, the client will select an alternative
  45. /// network protocol and re-try to connect.
  46. ///
  47. /// The fallback will use the default Name Server port as defined by ProtocolToNameServerPort.
  48. ///
  49. /// The fallback for TCP is UDP. All other protocols fallback to TCP.
  50. /// </remarks>
  51. public bool EnableProtocolFallback { get; set; }
  52. /// <summary>The address of last connected Name Server.</summary>
  53. public string NameServerAddress { get; private set; }
  54. /// <summary>The address of the actual chat server assigned from NameServer. Public for read only.</summary>
  55. public string FrontendAddress { get; private set; }
  56. /// <summary>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.</summary>
  57. private string chatRegion = "EU";
  58. /// <summary>Settable only before you connect! Defaults to "EU".</summary>
  59. public string ChatRegion
  60. {
  61. get { return this.chatRegion; }
  62. set { this.chatRegion = value; }
  63. }
  64. /// <summary>
  65. /// Defines a proxy URL for WebSocket connections. Can be the proxy or point to a .pac file.
  66. /// </summary>
  67. /// <remarks>
  68. /// This URL supports various definitions:
  69. ///
  70. /// "user:pass@proxyaddress:port"<br/>
  71. /// "proxyaddress:port"<br/>
  72. /// "system:"<br/>
  73. /// "pac:"<br/>
  74. /// "pac:http://host/path/pacfile.pac"<br/>
  75. ///
  76. /// Important: Don't define a protocol, except to point to a pac file. the proxy address should not begin with http:// or https://.
  77. /// </remarks>
  78. public string ProxyServerAddress;
  79. /// <summary>Current state of the ChatClient. Also use CanChat.</summary>
  80. public ChatState State { get; private set; }
  81. /// <summary> Disconnection cause. Check this inside <see cref="IChatClientListener.OnDisconnected"/>. </summary>
  82. public ChatDisconnectCause DisconnectedCause { get; private set; }
  83. /// <summary>
  84. /// Checks if this client is ready to send messages.
  85. /// </summary>
  86. public bool CanChat
  87. {
  88. get { return this.State == ChatState.ConnectedToFrontEnd && this.HasPeer; }
  89. }
  90. /// <summary>
  91. /// Checks if this client is ready to publish messages inside a public channel.
  92. /// </summary>
  93. /// <param name="channelName">The channel to do the check with.</param>
  94. /// <returns>Whether or not this client is ready to publish messages inside the public channel with the specified channelName.</returns>
  95. public bool CanChatInChannel(string channelName)
  96. {
  97. return this.CanChat && this.PublicChannels.ContainsKey(channelName) && !this.PublicChannelsUnsubscribing.Contains(channelName);
  98. }
  99. private bool HasPeer
  100. {
  101. get { return this.chatPeer != null; }
  102. }
  103. /// <summary>The version of your client. A new version also creates a new "virtual app" to separate players from older client versions.</summary>
  104. public string AppVersion { get; private set; }
  105. /// <summary>The AppID as assigned from the Photon Cloud.</summary>
  106. public string AppId { get; private set; }
  107. /// <summary>Settable only before you connect!</summary>
  108. public AuthenticationValues AuthValues { get; set; }
  109. /// <summary>The unique ID of a user/person, stored in AuthValues.UserId. Set it before you connect.</summary>
  110. /// <remarks>
  111. /// This value wraps AuthValues.UserId.
  112. /// It's not a nickname and we assume users with the same userID are the same person.</remarks>
  113. public string UserId
  114. {
  115. get
  116. {
  117. return (this.AuthValues != null) ? this.AuthValues.UserId : null;
  118. }
  119. private set
  120. {
  121. if (this.AuthValues == null)
  122. {
  123. this.AuthValues = new AuthenticationValues();
  124. }
  125. this.AuthValues.UserId = value;
  126. }
  127. }
  128. /// <summary>If greater than 0, new channels will limit the number of messages they cache locally.</summary>
  129. /// <remarks>
  130. /// This can be useful to limit the amount of memory used by chats.
  131. /// You can set a MessageLimit per channel but this value gets applied to new ones.
  132. ///
  133. /// Note:
  134. /// Changing this value, does not affect ChatChannels that are already in use!
  135. /// </remarks>
  136. public int MessageLimit;
  137. /// <summary>Limits the number of messages from private channel histories.</summary>
  138. /// <remarks>
  139. /// This is applied to all private channels on reconnect, as there is no explicit re-joining private channels.<br/>
  140. /// Default is -1, which gets available messages up to a maximum set by the server.<br/>
  141. /// A value of 0 gets you zero messages.<br/>
  142. /// The server's limit of messages may be lower. If so, the server's value will overrule this.<br/>
  143. /// </remarks>
  144. public int PrivateChatHistoryLength = -1;
  145. /// <summary> Public channels this client is subscribed to. </summary>
  146. public readonly Dictionary<string, ChatChannel> PublicChannels;
  147. /// <summary> Private channels in which this client has exchanged messages. </summary>
  148. public readonly Dictionary<string, ChatChannel> PrivateChannels;
  149. // channels being in unsubscribing process
  150. // items will be removed on successful unsubscription or subscription (the latter required after attempt to unsubscribe from not existing channel)
  151. private readonly HashSet<string> PublicChannelsUnsubscribing;
  152. private readonly IChatClientListener listener = null;
  153. /// <summary> The Chat Peer used by this client. </summary>
  154. public ChatPeer chatPeer = null;
  155. private const string ChatAppName = "chat";
  156. private bool didAuthenticate;
  157. private int? statusToSetWhenConnected;
  158. private object messageToSetWhenConnected;
  159. private int msDeltaForServiceCalls = 50;
  160. private int msTimestampOfLastServiceCall;
  161. /// <summary>Defines if a background thread will call SendOutgoingCommands, while your code calls Service to dispatch received messages.</summary>
  162. /// <remarks>
  163. /// The benefit of using a background thread to call SendOutgoingCommands is this:
  164. ///
  165. /// Even if your game logic is being paused, the background thread will keep the connection to the server up.
  166. /// On a lower level, acknowledgements and pings will prevent a server-side timeout while (e.g.) Unity loads assets.
  167. ///
  168. /// Your game logic still has to call Service regularly, or else incoming messages are not dispatched.
  169. /// As this typically triggers UI updates, it's easier to call Service from the main/UI thread.
  170. /// </remarks>
  171. public bool UseBackgroundWorkerForSending { get; set; }
  172. /// <summary>Exposes the TransportProtocol of the used PhotonPeer. Settable while not connected.</summary>
  173. public ConnectionProtocol TransportProtocol
  174. {
  175. get { return this.chatPeer.TransportProtocol; }
  176. set
  177. {
  178. if (this.chatPeer == null || this.chatPeer.PeerState != PeerStateValue.Disconnected)
  179. {
  180. this.listener.DebugReturn(DebugLevel.WARNING, "Can't set TransportProtocol. Disconnect first! " + ((this.chatPeer != null) ? "PeerState: " + this.chatPeer.PeerState : "The chatPeer is null."));
  181. return;
  182. }
  183. this.chatPeer.TransportProtocol = value;
  184. }
  185. }
  186. /// <summary>Defines which IPhotonSocket class to use per ConnectionProtocol.</summary>
  187. /// <remarks>
  188. /// Several platforms have special Socket implementations and slightly different APIs.
  189. /// To accomodate this, switching the socket implementation for a network protocol was made available.
  190. /// By default, UDP and TCP have socket implementations assigned.
  191. ///
  192. /// You only need to set the SocketImplementationConfig once, after creating a PhotonPeer
  193. /// and before connecting. If you switch the TransportProtocol, the correct implementation is being used.
  194. /// </remarks>
  195. public Dictionary<ConnectionProtocol, Type> SocketImplementationConfig
  196. {
  197. get { return this.chatPeer.SocketImplementationConfig; }
  198. }
  199. /// <summary>
  200. /// Chat client constructor.
  201. /// </summary>
  202. /// <param name="listener">The chat listener implementation.</param>
  203. /// <param name="protocol">Connection protocol to be used by this client. Default is <see cref="ConnectionProtocol.Udp"/>.</param>
  204. public ChatClient(IChatClientListener listener, ConnectionProtocol protocol = ConnectionProtocol.Udp)
  205. {
  206. this.listener = listener;
  207. this.State = ChatState.Uninitialized;
  208. this.chatPeer = new ChatPeer(this, protocol);
  209. this.chatPeer.SerializationProtocolType = SerializationProtocol.GpBinaryV18;
  210. this.PublicChannels = new Dictionary<string, ChatChannel>();
  211. this.PrivateChannels = new Dictionary<string, ChatChannel>();
  212. this.PublicChannelsUnsubscribing = new HashSet<string>();
  213. }
  214. public bool ConnectUsingSettings(ChatAppSettings appSettings)
  215. {
  216. if (appSettings == null)
  217. {
  218. this.listener.DebugReturn(DebugLevel.ERROR, "ConnectUsingSettings failed. The appSettings can't be null.'");
  219. return false;
  220. }
  221. if (!string.IsNullOrEmpty(appSettings.FixedRegion))
  222. {
  223. this.ChatRegion = appSettings.FixedRegion;
  224. }
  225. this.DebugOut = appSettings.NetworkLogging;
  226. this.TransportProtocol = appSettings.Protocol;
  227. this.EnableProtocolFallback = appSettings.EnableProtocolFallback;
  228. if (!appSettings.IsDefaultNameServer)
  229. {
  230. this.chatPeer.NameServerHost = appSettings.Server;
  231. this.chatPeer.NameServerPortOverride = appSettings.Port;
  232. }
  233. this.ProxyServerAddress = appSettings.ProxyServer;
  234. return this.Connect(appSettings.AppIdChat, appSettings.AppVersion, this.AuthValues);
  235. }
  236. /// <summary>
  237. /// Connects this client to the Photon Chat Cloud service, which will also authenticate the user (and set a UserId).
  238. /// </summary>
  239. /// <remarks>
  240. /// The ProxyServerAddress is used to connect. Set it before calling this method or use ConnectUsingSettings.
  241. /// </remarks>
  242. /// <param name="appId">Get your Photon Chat AppId from the <a href="https://dashboard.photonengine.com">Dashboard</a>.</param>
  243. /// <param name="appVersion">Any version string you make up. Used to separate users and variants of your clients, which might be incompatible.</param>
  244. /// <param name="authValues">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.</param>
  245. /// <returns></returns>
  246. public bool Connect(string appId, string appVersion, AuthenticationValues authValues)
  247. {
  248. this.chatPeer.TimePingInterval = 3000;
  249. this.DisconnectedCause = ChatDisconnectCause.None;
  250. if (authValues != null)
  251. {
  252. this.AuthValues = authValues;
  253. }
  254. this.AppId = appId;
  255. this.AppVersion = appVersion;
  256. this.didAuthenticate = false;
  257. this.chatPeer.QuickResendAttempts = 2;
  258. this.chatPeer.SentCountAllowance = 7;
  259. // clean all channels
  260. this.PublicChannels.Clear();
  261. this.PrivateChannels.Clear();
  262. this.PublicChannelsUnsubscribing.Clear();
  263. #if UNITY_WEBGL
  264. if (this.TransportProtocol == ConnectionProtocol.Tcp || this.TransportProtocol == ConnectionProtocol.Udp)
  265. {
  266. this.listener.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure.");
  267. this.TransportProtocol = ConnectionProtocol.WebSocketSecure;
  268. }
  269. #endif
  270. this.NameServerAddress = this.chatPeer.NameServerAddress;
  271. bool isConnecting = this.chatPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, "NameServer", null);
  272. if (isConnecting)
  273. {
  274. this.State = ChatState.ConnectingToNameServer;
  275. }
  276. if (this.UseBackgroundWorkerForSending)
  277. {
  278. #if UNITY_SWITCH
  279. SupportClass.StartBackgroundCalls(this.SendOutgoingInBackground, this.msDeltaForServiceCalls); // as workaround, we don't name the Thread.
  280. #else
  281. SupportClass.StartBackgroundCalls(this.SendOutgoingInBackground, this.msDeltaForServiceCalls, "ChatClient Service Thread");
  282. #endif
  283. }
  284. return isConnecting;
  285. }
  286. /// <summary>
  287. /// Connects this client to the Photon Chat Cloud service, which will also authenticate the user (and set a UserId).
  288. /// This also sets an online status once connected. By default it will set user status to <see cref="ChatUserStatus.Online"/>.
  289. /// See <see cref="SetOnlineStatus(int,object)"/> for more information.
  290. /// </summary>
  291. /// <param name="appId">Get your Photon Chat AppId from the <a href="https://dashboard.photonengine.com">Dashboard</a>.</param>
  292. /// <param name="appVersion">Any version string you make up. Used to separate users and variants of your clients, which might be incompatible.</param>
  293. /// <param name="authValues">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.</param>
  294. /// <param name="status">User status to set when connected. Predefined states are in class <see cref="ChatUserStatus"/>. Other values can be used at will.</param>
  295. /// <param name="message">Optional status Also sets a status-message which your friends can get.</param>
  296. /// <returns>If the connection attempt could be sent at all.</returns>
  297. public bool ConnectAndSetStatus(string appId, string appVersion, AuthenticationValues authValues,
  298. int status = ChatUserStatus.Online, object message = null)
  299. {
  300. statusToSetWhenConnected = status;
  301. messageToSetWhenConnected = message;
  302. return Connect(appId, appVersion, authValues);
  303. }
  304. /// <summary>
  305. /// Must be called regularly to keep connection between client and server alive and to process incoming messages.
  306. /// </summary>
  307. /// <remarks>
  308. /// This method limits the effort it does automatically using the private variable msDeltaForServiceCalls.
  309. /// That value is lower for connect and multiplied by 4 when chat-server connection is ready.
  310. /// </remarks>
  311. public void Service()
  312. {
  313. // Dispatch until every already-received message got dispatched
  314. while (this.HasPeer && this.chatPeer.DispatchIncomingCommands())
  315. {
  316. }
  317. // if there is no background thread for sending, Service() will do that as well, in intervals
  318. if (!this.UseBackgroundWorkerForSending)
  319. {
  320. if (Environment.TickCount - this.msTimestampOfLastServiceCall > this.msDeltaForServiceCalls || this.msTimestampOfLastServiceCall == 0)
  321. {
  322. this.msTimestampOfLastServiceCall = Environment.TickCount;
  323. while (this.HasPeer && this.chatPeer.SendOutgoingCommands())
  324. {
  325. }
  326. }
  327. }
  328. }
  329. /// <summary>
  330. /// Called by a separate thread, this sends outgoing commands of this peer, as long as it's connected.
  331. /// </summary>
  332. /// <returns>True as long as the client is not disconnected.</returns>
  333. private bool SendOutgoingInBackground()
  334. {
  335. while (this.HasPeer && this.chatPeer.SendOutgoingCommands())
  336. {
  337. }
  338. return this.State != ChatState.Disconnected;
  339. }
  340. /// <summary> Obsolete: Better use UseBackgroundWorkerForSending and Service(). </summary>
  341. [Obsolete("Better use UseBackgroundWorkerForSending and Service().")]
  342. public void SendAcksOnly()
  343. {
  344. if (this.HasPeer) this.chatPeer.SendAcksOnly();
  345. }
  346. /// <summary>
  347. /// Disconnects from the Chat Server by sending a "disconnect command", which prevents a timeout server-side.
  348. /// </summary>
  349. public void Disconnect(ChatDisconnectCause cause = ChatDisconnectCause.DisconnectByClientLogic)
  350. {
  351. if (this.HasPeer && this.chatPeer.PeerState != PeerStateValue.Disconnected)
  352. {
  353. this.State = ChatState.Disconnecting;
  354. this.DisconnectedCause = cause;
  355. this.chatPeer.Disconnect();
  356. }
  357. }
  358. /// <summary>
  359. /// Locally shuts down the connection to the Chat Server. This resets states locally but the server will have to timeout this peer.
  360. /// </summary>
  361. public void StopThread()
  362. {
  363. if (this.HasPeer)
  364. {
  365. this.chatPeer.StopThread();
  366. }
  367. }
  368. /// <summary>Sends operation to subscribe to a list of channels by name.</summary>
  369. /// <remarks>ChatClient.PublicChannels keeps track of the currently subscribed ChatChannels. Optionally, they can list the subscribers.</remarks>
  370. /// <param name="channels">List of channels to subscribe to. Avoid null or empty values.</param>
  371. /// <returns>If the operation could be sent at all (Example: Fails if not connected to Chat Server).</returns>
  372. public bool Subscribe(string[] channels)
  373. {
  374. return this.Subscribe(channels, 0);
  375. }
  376. /// <summary>
  377. /// Sends operation to subscribe to a list of channels by name and possibly retrieve messages we did not receive while unsubscribed.
  378. /// </summary>
  379. /// <remarks>ChatClient.PublicChannels keeps track of the currently subscribed ChatChannels. Optionally, they can list the subscribers.</remarks>
  380. /// <param name="channels">List of channels to subscribe to. Avoid null or empty values.</param>
  381. /// <param name="lastMsgIds">ID of last message received per channel. Useful when re subscribing to receive only messages we missed.</param>
  382. /// <returns>If the operation could be sent at all (Example: Fails if not connected to Chat Server).</returns>
  383. public bool Subscribe(string[] channels, int[] lastMsgIds)
  384. {
  385. if (!this.CanChat)
  386. {
  387. if (this.DebugOut >= DebugLevel.ERROR)
  388. {
  389. this.listener.DebugReturn(DebugLevel.ERROR, "Subscribe called while not connected to front end server.");
  390. }
  391. return false;
  392. }
  393. if (channels == null || channels.Length == 0)
  394. {
  395. if (this.DebugOut >= DebugLevel.WARNING)
  396. {
  397. this.listener.DebugReturn(DebugLevel.WARNING, "Subscribe can't be called for empty or null channels-list.");
  398. }
  399. return false;
  400. }
  401. for (int i = 0; i < channels.Length; i++)
  402. {
  403. if (string.IsNullOrEmpty(channels[i]))
  404. {
  405. if (this.DebugOut >= DebugLevel.ERROR)
  406. {
  407. this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Subscribe can't be called with a null or empty channel name at index {0}.", i));
  408. }
  409. return false;
  410. }
  411. }
  412. if (lastMsgIds == null || lastMsgIds.Length != channels.Length)
  413. {
  414. if (this.DebugOut >= DebugLevel.ERROR)
  415. {
  416. 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.");
  417. }
  418. return false;
  419. }
  420. Dictionary<byte, object> opParameters = new Dictionary<byte, object>
  421. {
  422. { ChatParameterCode.Channels, channels },
  423. { ChatParameterCode.MsgIds, lastMsgIds},
  424. { ChatParameterCode.HistoryLength, -1 } // server will decide how many messages to send to client
  425. };
  426. return this.chatPeer.SendOperation(ChatOperationCode.Subscribe, opParameters, SendOptions.SendReliable);
  427. }
  428. /// <summary>
  429. /// Sends operation to subscribe client to channels, optionally fetching a number of messages from the cache.
  430. /// </summary>
  431. /// <remarks>
  432. /// Subscribes channels will forward new messages to this user. Use PublishMessage to do so.
  433. /// The messages cache is limited but can be useful to get into ongoing conversations, if that's needed.
  434. ///
  435. /// ChatClient.PublicChannels keeps track of the currently subscribed ChatChannels. Optionally, they can list the subscribers.
  436. /// </remarks>
  437. /// <param name="channels">List of channels to subscribe to. Avoid null or empty values.</param>
  438. /// <param name="messagesFromHistory">0: no history. 1 and higher: number of messages in history. -1: all available history.</param>
  439. /// <returns>If the operation could be sent at all (Example: Fails if not connected to Chat Server).</returns>
  440. public bool Subscribe(string[] channels, int messagesFromHistory)
  441. {
  442. if (!this.CanChat)
  443. {
  444. if (this.DebugOut >= DebugLevel.ERROR)
  445. {
  446. this.listener.DebugReturn(DebugLevel.ERROR, "Subscribe called while not connected to front end server.");
  447. }
  448. return false;
  449. }
  450. if (channels == null || channels.Length == 0)
  451. {
  452. if (this.DebugOut >= DebugLevel.WARNING)
  453. {
  454. this.listener.DebugReturn(DebugLevel.WARNING, "Subscribe can't be called for empty or null channels-list.");
  455. }
  456. return false;
  457. }
  458. return this.SendChannelOperation(channels, (byte)ChatOperationCode.Subscribe, messagesFromHistory);
  459. }
  460. /// <summary>Unsubscribes from a list of channels, which stops getting messages from those.</summary>
  461. /// <remarks>
  462. /// The client will remove these channels from the PublicChannels dictionary once the server sent a response to this request.
  463. ///
  464. /// The request will be sent to the server and IChatClientListener.OnUnsubscribed gets called when the server
  465. /// actually removed the channel subscriptions.
  466. ///
  467. /// Unsubscribe will fail if you include null or empty channel names.
  468. /// </remarks>
  469. /// <param name="channels">Names of channels to unsubscribe.</param>
  470. /// <returns>False, if not connected to a chat server.</returns>
  471. public bool Unsubscribe(string[] channels)
  472. {
  473. if (!this.CanChat)
  474. {
  475. if (this.DebugOut >= DebugLevel.ERROR)
  476. {
  477. this.listener.DebugReturn(DebugLevel.ERROR, "Unsubscribe called while not connected to front end server.");
  478. }
  479. return false;
  480. }
  481. if (channels == null || channels.Length == 0)
  482. {
  483. if (this.DebugOut >= DebugLevel.WARNING)
  484. {
  485. this.listener.DebugReturn(DebugLevel.WARNING, "Unsubscribe can't be called for empty or null channels-list.");
  486. }
  487. return false;
  488. }
  489. foreach (string ch in channels)
  490. {
  491. this.PublicChannelsUnsubscribing.Add(ch);
  492. }
  493. return this.SendChannelOperation(channels, ChatOperationCode.Unsubscribe, 0);
  494. }
  495. /// <summary>Sends a message to a public channel which this client subscribed to.</summary>
  496. /// <remarks>
  497. /// Before you publish to a channel, you have to subscribe it.
  498. /// Everyone in that channel will get the message.
  499. /// </remarks>
  500. /// <param name="channelName">Name of the channel to publish to.</param>
  501. /// <param name="message">Your message (string or any serializable data).</param>
  502. /// <param name="forwardAsWebhook">Optionally, public messages can be forwarded as webhooks. Configure webhooks for your Chat app to use this.</param>
  503. /// <returns>False if the client is not yet ready to send messages.</returns>
  504. public bool PublishMessage(string channelName, object message, bool forwardAsWebhook = false)
  505. {
  506. return this.publishMessage(channelName, message, true, forwardAsWebhook);
  507. }
  508. internal bool PublishMessageUnreliable(string channelName, object message, bool forwardAsWebhook = false)
  509. {
  510. return this.publishMessage(channelName, message, false, forwardAsWebhook);
  511. }
  512. private bool publishMessage(string channelName, object message, bool reliable, bool forwardAsWebhook = false)
  513. {
  514. if (!this.CanChat)
  515. {
  516. if (this.DebugOut >= DebugLevel.ERROR)
  517. {
  518. this.listener.DebugReturn(DebugLevel.ERROR, "PublishMessage called while not connected to front end server.");
  519. }
  520. return false;
  521. }
  522. if (string.IsNullOrEmpty(channelName) || message == null)
  523. {
  524. if (this.DebugOut >= DebugLevel.WARNING)
  525. {
  526. this.listener.DebugReturn(DebugLevel.WARNING, "PublishMessage parameters must be non-null and not empty.");
  527. }
  528. return false;
  529. }
  530. Dictionary<byte, object> parameters = new Dictionary<byte, object>
  531. {
  532. { (byte)ChatParameterCode.Channel, channelName },
  533. { (byte)ChatParameterCode.Message, message }
  534. };
  535. if (forwardAsWebhook)
  536. {
  537. parameters.Add(ChatParameterCode.WebFlags, (byte)0x1);
  538. }
  539. return this.chatPeer.SendOperation(ChatOperationCode.Publish, parameters, new SendOptions() { Reliability = reliable });
  540. }
  541. /// <summary>
  542. /// Sends a private message to a single target user. Calls OnPrivateMessage on the receiving client.
  543. /// </summary>
  544. /// <param name="target">Username to send this message to.</param>
  545. /// <param name="message">The message you want to send. Can be a simple string or anything serializable.</param>
  546. /// <param name="forwardAsWebhook">Optionally, private messages can be forwarded as webhooks. Configure webhooks for your Chat app to use this.</param>
  547. /// <returns>True if this clients can send the message to the server.</returns>
  548. public bool SendPrivateMessage(string target, object message, bool forwardAsWebhook = false)
  549. {
  550. return this.SendPrivateMessage(target, message, false, forwardAsWebhook);
  551. }
  552. /// <summary>
  553. /// Sends a private message to a single target user. Calls OnPrivateMessage on the receiving client.
  554. /// </summary>
  555. /// <param name="target">Username to send this message to.</param>
  556. /// <param name="message">The message you want to send. Can be a simple string or anything serializable.</param>
  557. /// <param name="encrypt">Optionally, private messages can be encrypted. Encryption is not end-to-end as the server decrypts the message.</param>
  558. /// <param name="forwardAsWebhook">Optionally, private messages can be forwarded as webhooks. Configure webhooks for your Chat app to use this.</param>
  559. /// <returns>True if this clients can send the message to the server.</returns>
  560. public bool SendPrivateMessage(string target, object message, bool encrypt, bool forwardAsWebhook)
  561. {
  562. return this.sendPrivateMessage(target, message, encrypt, true, forwardAsWebhook);
  563. }
  564. internal bool SendPrivateMessageUnreliable(string target, object message, bool encrypt, bool forwardAsWebhook = false)
  565. {
  566. return this.sendPrivateMessage(target, message, encrypt, false, forwardAsWebhook);
  567. }
  568. private bool sendPrivateMessage(string target, object message, bool encrypt, bool reliable, bool forwardAsWebhook = false)
  569. {
  570. if (!this.CanChat)
  571. {
  572. if (this.DebugOut >= DebugLevel.ERROR)
  573. {
  574. this.listener.DebugReturn(DebugLevel.ERROR, "SendPrivateMessage called while not connected to front end server.");
  575. }
  576. return false;
  577. }
  578. if (string.IsNullOrEmpty(target) || message == null)
  579. {
  580. if (this.DebugOut >= DebugLevel.WARNING)
  581. {
  582. this.listener.DebugReturn(DebugLevel.WARNING, "SendPrivateMessage parameters must be non-null and not empty.");
  583. }
  584. return false;
  585. }
  586. Dictionary<byte, object> parameters = new Dictionary<byte, object>
  587. {
  588. { ChatParameterCode.UserId, target },
  589. { ChatParameterCode.Message, message }
  590. };
  591. if (forwardAsWebhook)
  592. {
  593. parameters.Add(ChatParameterCode.WebFlags, (byte)0x1);
  594. }
  595. return this.chatPeer.SendOperation(ChatOperationCode.SendPrivate, parameters, new SendOptions() { Reliability = reliable, Encrypt = encrypt });
  596. }
  597. /// <summary>Sets the user's status (pre-defined or custom) and an optional message.</summary>
  598. /// <remarks>
  599. /// The predefined status values can be found in class ChatUserStatus.
  600. /// State ChatUserStatus.Invisible will make you offline for everyone and send no message.
  601. ///
  602. /// You can set custom values in the status integer. Aside from the pre-configured ones,
  603. /// all states will be considered visible and online. Else, no one would see the custom state.
  604. ///
  605. /// The message object can be anything that Photon can serialize, including (but not limited to)
  606. /// Hashtable, object[] and string. This value is defined by your own conventions.
  607. /// </remarks>
  608. /// <param name="status">Predefined states are in class ChatUserStatus. Other values can be used at will.</param>
  609. /// <param name="message">Optional string message or null.</param>
  610. /// <param name="skipMessage">If true, the message gets ignored. It can be null but won't replace any current message.</param>
  611. /// <returns>True if the operation gets called on the server.</returns>
  612. private bool SetOnlineStatus(int status, object message, bool skipMessage)
  613. {
  614. if (!this.CanChat)
  615. {
  616. if (this.DebugOut >= DebugLevel.ERROR)
  617. {
  618. this.listener.DebugReturn(DebugLevel.ERROR, "SetOnlineStatus called while not connected to front end server.");
  619. }
  620. return false;
  621. }
  622. Dictionary<byte, object> parameters = new Dictionary<byte, object>
  623. {
  624. { ChatParameterCode.Status, status },
  625. };
  626. if (skipMessage)
  627. {
  628. parameters[ChatParameterCode.SkipMessage] = true;
  629. }
  630. else
  631. {
  632. parameters[ChatParameterCode.Message] = message;
  633. }
  634. return this.chatPeer.SendOperation(ChatOperationCode.UpdateStatus, parameters, SendOptions.SendReliable);
  635. }
  636. /// <summary>Sets the user's status without changing your status-message.</summary>
  637. /// <remarks>
  638. /// The predefined status values can be found in class ChatUserStatus.
  639. /// State ChatUserStatus.Invisible will make you offline for everyone and send no message.
  640. ///
  641. /// You can set custom values in the status integer. Aside from the pre-configured ones,
  642. /// all states will be considered visible and online. Else, no one would see the custom state.
  643. ///
  644. /// This overload does not change the set message.
  645. /// </remarks>
  646. /// <param name="status">Predefined states are in class ChatUserStatus. Other values can be used at will.</param>
  647. /// <returns>True if the operation gets called on the server.</returns>
  648. public bool SetOnlineStatus(int status)
  649. {
  650. return this.SetOnlineStatus(status, null, true);
  651. }
  652. /// <summary>Sets the user's status without changing your status-message.</summary>
  653. /// <remarks>
  654. /// The predefined status values can be found in class ChatUserStatus.
  655. /// State ChatUserStatus.Invisible will make you offline for everyone and send no message.
  656. ///
  657. /// You can set custom values in the status integer. Aside from the pre-configured ones,
  658. /// all states will be considered visible and online. Else, no one would see the custom state.
  659. ///
  660. /// The message object can be anything that Photon can serialize, including (but not limited to)
  661. /// Hashtable, object[] and string. This value is defined by your own conventions.
  662. /// </remarks>
  663. /// <param name="status">Predefined states are in class ChatUserStatus. Other values can be used at will.</param>
  664. /// <param name="message">Also sets a status-message which your friends can get.</param>
  665. /// <returns>True if the operation gets called on the server.</returns>
  666. public bool SetOnlineStatus(int status, object message)
  667. {
  668. return this.SetOnlineStatus(status, message, false);
  669. }
  670. /// <summary>
  671. /// Adds friends to a list on the Chat Server which will send you status updates for those.
  672. /// </summary>
  673. /// <remarks>
  674. /// AddFriends and RemoveFriends enable clients to handle their friend list
  675. /// in the Photon Chat server. Having users on your friends list gives you access
  676. /// to their current online status (and whatever info your client sets in it).
  677. ///
  678. /// Each user can set an online status consisting of an integer and an arbitrary
  679. /// (serializable) object. The object can be null, Hashtable, object[] or anything
  680. /// else Photon can serialize.
  681. ///
  682. /// The status is published automatically to friends (anyone who set your user ID
  683. /// with AddFriends).
  684. ///
  685. /// Photon flushes friends-list when a chat client disconnects, so it has to be
  686. /// set each time. If your community API gives you access to online status already,
  687. /// you could filter and set online friends in AddFriends.
  688. ///
  689. /// Actual friend relations are not persistent and have to be stored outside
  690. /// of Photon.
  691. /// </remarks>
  692. /// <param name="friends">Array of friend userIds.</param>
  693. /// <returns>If the operation could be sent.</returns>
  694. public bool AddFriends(string[] friends)
  695. {
  696. if (!this.CanChat)
  697. {
  698. if (this.DebugOut >= DebugLevel.ERROR)
  699. {
  700. this.listener.DebugReturn(DebugLevel.ERROR, "AddFriends called while not connected to front end server.");
  701. }
  702. return false;
  703. }
  704. if (friends == null || friends.Length == 0)
  705. {
  706. if (this.DebugOut >= DebugLevel.WARNING)
  707. {
  708. this.listener.DebugReturn(DebugLevel.WARNING, "AddFriends can't be called for empty or null list.");
  709. }
  710. return false;
  711. }
  712. if (friends.Length > FriendRequestListMax)
  713. {
  714. if (this.DebugOut >= DebugLevel.WARNING)
  715. {
  716. this.listener.DebugReturn(DebugLevel.WARNING, "AddFriends max list size exceeded: " + friends.Length + " > " + FriendRequestListMax);
  717. }
  718. return false;
  719. }
  720. Dictionary<byte, object> parameters = new Dictionary<byte, object>
  721. {
  722. { ChatParameterCode.Friends, friends },
  723. };
  724. return this.chatPeer.SendOperation(ChatOperationCode.AddFriends, parameters, SendOptions.SendReliable);
  725. }
  726. /// <summary>
  727. /// Removes the provided entries from the list on the Chat Server and stops their status updates.
  728. /// </summary>
  729. /// <remarks>
  730. /// Photon flushes friends-list when a chat client disconnects. Unless you want to
  731. /// remove individual entries, you don't have to RemoveFriends.
  732. ///
  733. /// AddFriends and RemoveFriends enable clients to handle their friend list
  734. /// in the Photon Chat server. Having users on your friends list gives you access
  735. /// to their current online status (and whatever info your client sets in it).
  736. ///
  737. /// Each user can set an online status consisting of an integer and an arbitratry
  738. /// (serializable) object. The object can be null, Hashtable, object[] or anything
  739. /// else Photon can serialize.
  740. ///
  741. /// The status is published automatically to friends (anyone who set your user ID
  742. /// with AddFriends).
  743. ///
  744. /// Photon flushes friends-list when a chat client disconnects, so it has to be
  745. /// set each time. If your community API gives you access to online status already,
  746. /// you could filter and set online friends in AddFriends.
  747. ///
  748. /// Actual friend relations are not persistent and have to be stored outside
  749. /// of Photon.
  750. ///
  751. /// AddFriends and RemoveFriends enable clients to handle their friend list
  752. /// in the Photon Chat server. Having users on your friends list gives you access
  753. /// to their current online status (and whatever info your client sets in it).
  754. ///
  755. /// Each user can set an online status consisting of an integer and an arbitratry
  756. /// (serializable) object. The object can be null, Hashtable, object[] or anything
  757. /// else Photon can serialize.
  758. ///
  759. /// The status is published automatically to friends (anyone who set your user ID
  760. /// with AddFriends).
  761. ///
  762. ///
  763. /// Actual friend relations are not persistent and have to be stored outside
  764. /// of Photon.
  765. /// </remarks>
  766. /// <param name="friends">Array of friend userIds.</param>
  767. /// <returns>If the operation could be sent.</returns>
  768. public bool RemoveFriends(string[] friends)
  769. {
  770. if (!this.CanChat)
  771. {
  772. if (this.DebugOut >= DebugLevel.ERROR)
  773. {
  774. this.listener.DebugReturn(DebugLevel.ERROR, "RemoveFriends called while not connected to front end server.");
  775. }
  776. return false;
  777. }
  778. if (friends == null || friends.Length == 0)
  779. {
  780. if (this.DebugOut >= DebugLevel.WARNING)
  781. {
  782. this.listener.DebugReturn(DebugLevel.WARNING, "RemoveFriends can't be called for empty or null list.");
  783. }
  784. return false;
  785. }
  786. if (friends.Length > FriendRequestListMax)
  787. {
  788. if (this.DebugOut >= DebugLevel.WARNING)
  789. {
  790. this.listener.DebugReturn(DebugLevel.WARNING, "RemoveFriends max list size exceeded: " + friends.Length + " > " + FriendRequestListMax);
  791. }
  792. return false;
  793. }
  794. Dictionary<byte, object> parameters = new Dictionary<byte, object>
  795. {
  796. { ChatParameterCode.Friends, friends },
  797. };
  798. return this.chatPeer.SendOperation(ChatOperationCode.RemoveFriends, parameters, SendOptions.SendReliable);
  799. }
  800. /// <summary>
  801. /// Get you the (locally used) channel name for the chat between this client and another user.
  802. /// </summary>
  803. /// <param name="userName">Remote user's name or UserId.</param>
  804. /// <returns>The (locally used) channel name for a private channel.</returns>
  805. /// <remarks>Do not subscribe to this channel.
  806. /// Private channels do not need to be explicitly subscribed to.
  807. /// Use this for debugging purposes mainly.</remarks>
  808. public string GetPrivateChannelNameByUser(string userName)
  809. {
  810. return string.Format("{0}:{1}", this.UserId, userName);
  811. }
  812. /// <summary>
  813. /// Simplified access to either private or public channels by name.
  814. /// </summary>
  815. /// <param name="channelName">Name of the channel to get. For private channels, the channel-name is composed of both user's names.</param>
  816. /// <param name="isPrivate">Define if you expect a private or public channel.</param>
  817. /// <param name="channel">Out parameter gives you the found channel, if any.</param>
  818. /// <returns>True if the channel was found.</returns>
  819. /// <remarks>Public channels exist only when subscribed to them.
  820. /// Private channels exist only when at least one message is exchanged with the target user privately.</remarks>
  821. public bool TryGetChannel(string channelName, bool isPrivate, out ChatChannel channel)
  822. {
  823. if (!isPrivate)
  824. {
  825. return this.PublicChannels.TryGetValue(channelName, out channel);
  826. }
  827. else
  828. {
  829. return this.PrivateChannels.TryGetValue(channelName, out channel);
  830. }
  831. }
  832. /// <summary>
  833. /// Simplified access to all channels by name. Checks public channels first, then private ones.
  834. /// </summary>
  835. /// <param name="channelName">Name of the channel to get.</param>
  836. /// <param name="channel">Out parameter gives you the found channel, if any.</param>
  837. /// <returns>True if the channel was found.</returns>
  838. /// <remarks>Public channels exist only when subscribed to them.
  839. /// Private channels exist only when at least one message is exchanged with the target user privately.</remarks>
  840. public bool TryGetChannel(string channelName, out ChatChannel channel)
  841. {
  842. bool found = false;
  843. found = this.PublicChannels.TryGetValue(channelName, out channel);
  844. if (found) return true;
  845. found = this.PrivateChannels.TryGetValue(channelName, out channel);
  846. return found;
  847. }
  848. /// <summary>
  849. /// Simplified access to private channels by target user.
  850. /// </summary>
  851. /// <param name="userId">UserId of the target user in the private channel.</param>
  852. /// <param name="channel">Out parameter gives you the found channel, if any.</param>
  853. /// <returns>True if the channel was found.</returns>
  854. public bool TryGetPrivateChannelByUser(string userId, out ChatChannel channel)
  855. {
  856. channel = null;
  857. if (string.IsNullOrEmpty(userId))
  858. {
  859. return false;
  860. }
  861. string channelName = this.GetPrivateChannelNameByUser(userId);
  862. return this.TryGetChannel(channelName, true, out channel);
  863. }
  864. /// <summary>
  865. /// Sets the level (and amount) of debug output provided by the library.
  866. /// </summary>
  867. /// <remarks>
  868. /// This affects the callbacks to IChatClientListener.DebugReturn.
  869. /// Default Level: Error.
  870. /// </remarks>
  871. public DebugLevel DebugOut
  872. {
  873. set { this.chatPeer.DebugOut = value; }
  874. get { return this.chatPeer.DebugOut; }
  875. }
  876. #region Private methods area
  877. #region IPhotonPeerListener implementation
  878. void IPhotonPeerListener.DebugReturn(DebugLevel level, string message)
  879. {
  880. this.listener.DebugReturn(level, message);
  881. }
  882. void IPhotonPeerListener.OnEvent(EventData eventData)
  883. {
  884. switch (eventData.Code)
  885. {
  886. case ChatEventCode.ChatMessages:
  887. this.HandleChatMessagesEvent(eventData);
  888. break;
  889. case ChatEventCode.PrivateMessage:
  890. this.HandlePrivateMessageEvent(eventData);
  891. break;
  892. case ChatEventCode.StatusUpdate:
  893. this.HandleStatusUpdate(eventData);
  894. break;
  895. case ChatEventCode.Subscribe:
  896. this.HandleSubscribeEvent(eventData);
  897. break;
  898. case ChatEventCode.Unsubscribe:
  899. this.HandleUnsubscribeEvent(eventData);
  900. break;
  901. case ChatEventCode.UserSubscribed:
  902. this.HandleUserSubscribedEvent(eventData);
  903. break;
  904. case ChatEventCode.UserUnsubscribed:
  905. this.HandleUserUnsubscribedEvent(eventData);
  906. break;
  907. #if CHAT_EXTENDED
  908. case ChatEventCode.PropertiesChanged:
  909. this.HandlePropertiesChanged(eventData);
  910. break;
  911. case ChatEventCode.ErrorInfo:
  912. this.HandleErrorInfoEvent(eventData);
  913. break;
  914. #endif
  915. }
  916. }
  917. void IPhotonPeerListener.OnOperationResponse(OperationResponse operationResponse)
  918. {
  919. switch (operationResponse.OperationCode)
  920. {
  921. case (byte)ChatOperationCode.Authenticate:
  922. this.HandleAuthResponse(operationResponse);
  923. break;
  924. // the following operations usually don't return useful data and no error.
  925. case (byte)ChatOperationCode.Subscribe:
  926. case (byte)ChatOperationCode.Unsubscribe:
  927. case (byte)ChatOperationCode.Publish:
  928. case (byte)ChatOperationCode.SendPrivate:
  929. default:
  930. if ((operationResponse.ReturnCode != 0) && (this.DebugOut >= DebugLevel.ERROR))
  931. {
  932. if (operationResponse.ReturnCode == -2)
  933. {
  934. 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));
  935. }
  936. else
  937. {
  938. this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Chat Operation {0} failed (Code: {1}). Debug Message: {2}", operationResponse.OperationCode, operationResponse.ReturnCode, operationResponse.DebugMessage));
  939. }
  940. }
  941. break;
  942. }
  943. }
  944. void IPhotonPeerListener.OnStatusChanged(StatusCode statusCode)
  945. {
  946. switch (statusCode)
  947. {
  948. case StatusCode.Connect:
  949. if (!this.chatPeer.IsProtocolSecure)
  950. {
  951. if (!this.chatPeer.EstablishEncryption())
  952. {
  953. if (this.DebugOut >= DebugLevel.ERROR)
  954. {
  955. this.listener.DebugReturn(DebugLevel.ERROR, "Error establishing encryption");
  956. }
  957. }
  958. }
  959. else
  960. {
  961. this.TryAuthenticateOnNameServer();
  962. }
  963. if (this.State == ChatState.ConnectingToNameServer)
  964. {
  965. this.State = ChatState.ConnectedToNameServer;
  966. this.listener.OnChatStateChange(this.State);
  967. }
  968. else if (this.State == ChatState.ConnectingToFrontEnd)
  969. {
  970. if (!this.AuthenticateOnFrontEnd())
  971. {
  972. if (this.DebugOut >= DebugLevel.ERROR)
  973. {
  974. this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Error authenticating on frontend! Check log output, AuthValues and if you're connected. State: {0}", this.State));
  975. }
  976. }
  977. }
  978. break;
  979. case StatusCode.EncryptionEstablished:
  980. // once encryption is available, the client should send one (secure) authenticate. it includes the AppId (which identifies your app on the Photon Cloud)
  981. this.TryAuthenticateOnNameServer();
  982. break;
  983. case StatusCode.Disconnect:
  984. switch (this.State)
  985. {
  986. case ChatState.ConnectWithFallbackProtocol:
  987. this.EnableProtocolFallback = false; // the client does a fallback only one time
  988. this.chatPeer.NameServerPortOverride = 0; // resets a value in the peer only (as we change the protocol, the port has to change, too)
  989. this.chatPeer.TransportProtocol = (this.chatPeer.TransportProtocol == ConnectionProtocol.Tcp) ? ConnectionProtocol.Udp : ConnectionProtocol.Tcp;
  990. this.Connect(this.AppId, this.AppVersion, null);
  991. // the client now has to return, instead of break, to avoid further processing of the disconnect call
  992. return;
  993. case ChatState.Authenticated:
  994. this.ConnectToFrontEnd();
  995. // client disconnected from nameserver after authentication
  996. // to switch to frontend
  997. return;
  998. case ChatState.Disconnecting:
  999. // expected disconnect
  1000. break;
  1001. default:
  1002. // unexpected disconnect, we log warning and stacktrace
  1003. string stacktrace = string.Empty;
  1004. #if DEBUG && !NETFX_CORE
  1005. stacktrace = new System.Diagnostics.StackTrace(true).ToString();
  1006. #endif
  1007. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Got a unexpected Disconnect in ChatState: {0}. Server: {1} Trace: {2}", this.State, this.chatPeer.ServerAddress, stacktrace));
  1008. break;
  1009. }
  1010. if (this.AuthValues != null)
  1011. {
  1012. this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
  1013. }
  1014. this.State = ChatState.Disconnected;
  1015. this.listener.OnChatStateChange(ChatState.Disconnected);
  1016. this.listener.OnDisconnected();
  1017. break;
  1018. case StatusCode.DisconnectByServerUserLimit:
  1019. this.listener.DebugReturn(DebugLevel.ERROR, "This connection was rejected due to the apps CCU limit.");
  1020. this.Disconnect(ChatDisconnectCause.MaxCcuReached);
  1021. break;
  1022. case StatusCode.ExceptionOnConnect:
  1023. case StatusCode.SecurityExceptionOnConnect:
  1024. case StatusCode.EncryptionFailedToEstablish:
  1025. this.DisconnectedCause = ChatDisconnectCause.ExceptionOnConnect;
  1026. // if enabled, the client can attempt to connect with another networking-protocol to check if that connects
  1027. if (this.EnableProtocolFallback && this.State == ChatState.ConnectingToNameServer)
  1028. {
  1029. this.State = ChatState.ConnectWithFallbackProtocol;
  1030. }
  1031. else
  1032. {
  1033. this.Disconnect(ChatDisconnectCause.ExceptionOnConnect);
  1034. }
  1035. break;
  1036. case StatusCode.Exception:
  1037. case StatusCode.ExceptionOnReceive:
  1038. this.Disconnect(ChatDisconnectCause.Exception);
  1039. break;
  1040. case StatusCode.DisconnectByServerTimeout:
  1041. this.Disconnect(ChatDisconnectCause.ServerTimeout);
  1042. break;
  1043. case StatusCode.DisconnectByServerLogic:
  1044. this.Disconnect(ChatDisconnectCause.DisconnectByServerLogic);
  1045. break;
  1046. case StatusCode.DisconnectByServerReasonUnknown:
  1047. this.Disconnect(ChatDisconnectCause.DisconnectByServerReasonUnknown);
  1048. break;
  1049. case StatusCode.TimeoutDisconnect:
  1050. this.DisconnectedCause = ChatDisconnectCause.ClientTimeout;
  1051. // if enabled, the client can attempt to connect with another networking-protocol to check if that connects
  1052. if (this.EnableProtocolFallback && this.State == ChatState.ConnectingToNameServer)
  1053. {
  1054. this.State = ChatState.ConnectWithFallbackProtocol;
  1055. }
  1056. else
  1057. {
  1058. this.Disconnect(ChatDisconnectCause.ClientTimeout);
  1059. }
  1060. break;
  1061. }
  1062. }
  1063. #if SDK_V4
  1064. void IPhotonPeerListener.OnMessage(object msg)
  1065. {
  1066. string channelName = null;
  1067. var receivedBytes = (byte[])msg;
  1068. var channelId = BitConverter.ToInt32(receivedBytes, 0);
  1069. var messageBytes = new byte[receivedBytes.Length - 4];
  1070. Array.Copy(receivedBytes, 4, messageBytes, 0, receivedBytes.Length - 4);
  1071. foreach (var channel in this.PublicChannels)
  1072. {
  1073. if (channel.Value.ChannelID == channelId)
  1074. {
  1075. channelName = channel.Key;
  1076. break;
  1077. }
  1078. }
  1079. if (channelName != null)
  1080. {
  1081. this.listener.DebugReturn(DebugLevel.ALL, string.Format("got OnMessage in channel {0}", channelName));
  1082. }
  1083. else
  1084. {
  1085. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("got OnMessage in unknown channel {0}", channelId));
  1086. }
  1087. this.listener.OnReceiveBroadcastMessage(channelName, messageBytes);
  1088. }
  1089. #endif
  1090. #endregion
  1091. private void TryAuthenticateOnNameServer()
  1092. {
  1093. if (!this.didAuthenticate)
  1094. {
  1095. this.didAuthenticate = this.chatPeer.AuthenticateOnNameServer(this.AppId, this.AppVersion, this.ChatRegion, this.AuthValues);
  1096. if (!this.didAuthenticate)
  1097. {
  1098. if (this.DebugOut >= DebugLevel.ERROR)
  1099. {
  1100. 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));
  1101. }
  1102. }
  1103. }
  1104. }
  1105. private bool SendChannelOperation(string[] channels, byte operation, int historyLength)
  1106. {
  1107. Dictionary<byte, object> opParameters = new Dictionary<byte, object> { { (byte)ChatParameterCode.Channels, channels } };
  1108. if (historyLength != 0)
  1109. {
  1110. opParameters.Add((byte)ChatParameterCode.HistoryLength, historyLength);
  1111. }
  1112. return this.chatPeer.SendOperation(operation, opParameters, SendOptions.SendReliable);
  1113. }
  1114. private void HandlePrivateMessageEvent(EventData eventData)
  1115. {
  1116. //Console.WriteLine(SupportClass.DictionaryToString(eventData.Parameters));
  1117. object message = (object)eventData.Parameters[(byte)ChatParameterCode.Message];
  1118. string sender = (string)eventData.Parameters[(byte)ChatParameterCode.Sender];
  1119. int msgId = (int)eventData.Parameters[ChatParameterCode.MsgId];
  1120. string channelName;
  1121. if (this.UserId != null && this.UserId.Equals(sender))
  1122. {
  1123. string target = (string)eventData.Parameters[(byte)ChatParameterCode.UserId];
  1124. channelName = this.GetPrivateChannelNameByUser(target);
  1125. }
  1126. else
  1127. {
  1128. channelName = this.GetPrivateChannelNameByUser(sender);
  1129. }
  1130. ChatChannel channel;
  1131. if (!this.PrivateChannels.TryGetValue(channelName, out channel))
  1132. {
  1133. channel = new ChatChannel(channelName);
  1134. channel.IsPrivate = true;
  1135. channel.MessageLimit = this.MessageLimit;
  1136. this.PrivateChannels.Add(channel.Name, channel);
  1137. }
  1138. channel.Add(sender, message, msgId);
  1139. this.listener.OnPrivateMessage(sender, message, channelName);
  1140. }
  1141. private void HandleChatMessagesEvent(EventData eventData)
  1142. {
  1143. object[] messages = (object[])eventData.Parameters[(byte)ChatParameterCode.Messages];
  1144. string[] senders = (string[])eventData.Parameters[(byte)ChatParameterCode.Senders];
  1145. string channelName = (string)eventData.Parameters[(byte)ChatParameterCode.Channel];
  1146. int lastMsgId = (int)eventData.Parameters[ChatParameterCode.MsgId];
  1147. ChatChannel channel;
  1148. if (!this.PublicChannels.TryGetValue(channelName, out channel))
  1149. {
  1150. if (this.DebugOut >= DebugLevel.WARNING)
  1151. {
  1152. this.listener.DebugReturn(DebugLevel.WARNING, "Channel " + channelName + " for incoming message event not found.");
  1153. }
  1154. return;
  1155. }
  1156. channel.Add(senders, messages, lastMsgId);
  1157. this.listener.OnGetMessages(channelName, senders, messages);
  1158. }
  1159. private void HandleSubscribeEvent(EventData eventData)
  1160. {
  1161. string[] channelsInResponse = (string[])eventData.Parameters[ChatParameterCode.Channels];
  1162. bool[] results = (bool[])eventData.Parameters[ChatParameterCode.SubscribeResults];
  1163. for (int i = 0; i < channelsInResponse.Length; i++)
  1164. {
  1165. if (results[i])
  1166. {
  1167. string channelName = channelsInResponse[i];
  1168. ChatChannel channel;
  1169. if (!this.PublicChannels.TryGetValue(channelName, out channel))
  1170. {
  1171. channel = new ChatChannel(channelName);
  1172. channel.MessageLimit = this.MessageLimit;
  1173. this.PublicChannels.Add(channel.Name, channel);
  1174. }
  1175. object temp;
  1176. if (eventData.Parameters.TryGetValue(ChatParameterCode.Properties, out temp))
  1177. {
  1178. Dictionary<object, object> channelProperties = temp as Dictionary<object, object>;
  1179. channel.ReadChannelProperties(channelProperties);
  1180. }
  1181. if (channel.PublishSubscribers) // or maybe remove check & always add anyway?
  1182. {
  1183. channel.AddSubscriber(this.UserId);
  1184. }
  1185. if (eventData.Parameters.TryGetValue(ChatParameterCode.ChannelSubscribers, out temp))
  1186. {
  1187. string[] subscribers = temp as string[];
  1188. channel.AddSubscribers(subscribers);
  1189. }
  1190. #if CHAT_EXTENDED
  1191. if (eventData.Parameters.TryGetValue(ChatParameterCode.UserProperties, out temp))
  1192. {
  1193. //UnityEngine.Debug.LogFormat("temp = {0}", temp);
  1194. Dictionary<string, object> userProperties = temp as Dictionary<string, object>;
  1195. foreach (var pair in userProperties)
  1196. {
  1197. channel.ReadUserProperties(pair.Key, pair.Value as Dictionary<object, object>);
  1198. }
  1199. }
  1200. #endif
  1201. }
  1202. }
  1203. this.listener.OnSubscribed(channelsInResponse, results);
  1204. }
  1205. private void HandleUnsubscribeEvent(EventData eventData)
  1206. {
  1207. string[] channelsInRequest = (string[])eventData[ChatParameterCode.Channels];
  1208. for (int i = 0; i < channelsInRequest.Length; i++)
  1209. {
  1210. string channelName = channelsInRequest[i];
  1211. this.PublicChannels.Remove(channelName);
  1212. this.PublicChannelsUnsubscribing.Remove(channelName);
  1213. }
  1214. this.listener.OnUnsubscribed(channelsInRequest);
  1215. }
  1216. private void HandleAuthResponse(OperationResponse operationResponse)
  1217. {
  1218. if (this.DebugOut >= DebugLevel.INFO)
  1219. {
  1220. this.listener.DebugReturn(DebugLevel.INFO, operationResponse.ToStringFull() + " on: " + this.chatPeer.NameServerAddress);
  1221. }
  1222. if (operationResponse.ReturnCode == 0)
  1223. {
  1224. if (this.State == ChatState.ConnectedToNameServer)
  1225. {
  1226. this.State = ChatState.Authenticated;
  1227. this.listener.OnChatStateChange(this.State);
  1228. if (operationResponse.Parameters.ContainsKey(ParameterCode.Secret))
  1229. {
  1230. if (this.AuthValues == null)
  1231. {
  1232. this.AuthValues = new AuthenticationValues();
  1233. }
  1234. this.AuthValues.Token = operationResponse[ParameterCode.Secret];
  1235. this.FrontendAddress = (string)operationResponse[ParameterCode.Address];
  1236. // we disconnect and status handler starts to connect to front end
  1237. this.chatPeer.Disconnect();
  1238. }
  1239. else
  1240. {
  1241. if (this.DebugOut >= DebugLevel.ERROR)
  1242. {
  1243. this.listener.DebugReturn(DebugLevel.ERROR, "No secret in authentication response.");
  1244. }
  1245. }
  1246. if (operationResponse.Parameters.ContainsKey(ParameterCode.UserId))
  1247. {
  1248. string incomingId = operationResponse.Parameters[ParameterCode.UserId] as string;
  1249. if (!string.IsNullOrEmpty(incomingId))
  1250. {
  1251. this.UserId = incomingId;
  1252. this.listener.DebugReturn(DebugLevel.INFO, string.Format("Received your UserID from server. Updating local value to: {0}", this.UserId));
  1253. }
  1254. }
  1255. }
  1256. else if (this.State == ChatState.ConnectingToFrontEnd)
  1257. {
  1258. this.State = ChatState.ConnectedToFrontEnd;
  1259. this.listener.OnChatStateChange(this.State);
  1260. this.listener.OnConnected();
  1261. if (statusToSetWhenConnected.HasValue)
  1262. {
  1263. SetOnlineStatus(statusToSetWhenConnected.Value, messageToSetWhenConnected);
  1264. statusToSetWhenConnected = null;
  1265. }
  1266. }
  1267. }
  1268. else
  1269. {
  1270. //this.listener.DebugReturn(DebugLevel.INFO, operationResponse.ToStringFull() + " NS: " + this.NameServerAddress + " FrontEnd: " + this.frontEndAddress);
  1271. switch (operationResponse.ReturnCode)
  1272. {
  1273. case ErrorCode.InvalidAuthentication:
  1274. this.DisconnectedCause = ChatDisconnectCause.InvalidAuthentication;
  1275. break;
  1276. case ErrorCode.CustomAuthenticationFailed:
  1277. this.DisconnectedCause = ChatDisconnectCause.CustomAuthenticationFailed;
  1278. break;
  1279. case ErrorCode.InvalidRegion:
  1280. this.DisconnectedCause = ChatDisconnectCause.InvalidRegion;
  1281. break;
  1282. case ErrorCode.MaxCcuReached:
  1283. this.DisconnectedCause = ChatDisconnectCause.MaxCcuReached;
  1284. break;
  1285. case ErrorCode.OperationNotAllowedInCurrentState:
  1286. this.DisconnectedCause = ChatDisconnectCause.OperationNotAllowedInCurrentState;
  1287. break;
  1288. case ErrorCode.AuthenticationTicketExpired:
  1289. this.DisconnectedCause = ChatDisconnectCause.AuthenticationTicketExpired;
  1290. break;
  1291. }
  1292. if (this.DebugOut >= DebugLevel.ERROR)
  1293. {
  1294. this.listener.DebugReturn(DebugLevel.ERROR, string.Format("{0} ClientState: {1} ServerAddress: {2}", operationResponse.ToStringFull(), this.State, this.chatPeer.ServerAddress));
  1295. }
  1296. this.Disconnect(this.DisconnectedCause);
  1297. }
  1298. }
  1299. private void HandleStatusUpdate(EventData eventData)
  1300. {
  1301. string user = (string)eventData.Parameters[ChatParameterCode.Sender];
  1302. int status = (int)eventData.Parameters[ChatParameterCode.Status];
  1303. object message = null;
  1304. bool gotMessage = eventData.Parameters.ContainsKey(ChatParameterCode.Message);
  1305. if (gotMessage)
  1306. {
  1307. message = eventData.Parameters[ChatParameterCode.Message];
  1308. }
  1309. this.listener.OnStatusUpdate(user, status, gotMessage, message);
  1310. }
  1311. private bool ConnectToFrontEnd()
  1312. {
  1313. this.State = ChatState.ConnectingToFrontEnd;
  1314. if (this.DebugOut >= DebugLevel.INFO)
  1315. {
  1316. this.listener.DebugReturn(DebugLevel.INFO, "Connecting to frontend " + this.FrontendAddress);
  1317. }
  1318. #if UNITY_WEBGL
  1319. if (this.TransportProtocol == ConnectionProtocol.Tcp || this.TransportProtocol == ConnectionProtocol.Udp)
  1320. {
  1321. this.listener.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure.");
  1322. this.TransportProtocol = ConnectionProtocol.WebSocketSecure;
  1323. }
  1324. #endif
  1325. if (!this.chatPeer.Connect(this.FrontendAddress, this.ProxyServerAddress, ChatAppName, null))
  1326. {
  1327. if (this.DebugOut >= DebugLevel.ERROR)
  1328. {
  1329. this.listener.DebugReturn(DebugLevel.ERROR, string.Format("Connecting to frontend {0} failed.", this.FrontendAddress));
  1330. }
  1331. return false;
  1332. }
  1333. return true;
  1334. }
  1335. private bool AuthenticateOnFrontEnd()
  1336. {
  1337. if (this.AuthValues != null)
  1338. {
  1339. if (this.AuthValues.Token == null)
  1340. {
  1341. if (this.DebugOut >= DebugLevel.ERROR)
  1342. {
  1343. this.listener.DebugReturn(DebugLevel.ERROR, "Can't authenticate on front end server. Secret (AuthValues.Token) is not set");
  1344. }
  1345. return false;
  1346. }
  1347. else
  1348. {
  1349. Dictionary<byte, object> opParameters = new Dictionary<byte, object> { { (byte)ChatParameterCode.Secret, this.AuthValues.Token } };
  1350. if (this.PrivateChatHistoryLength > -1)
  1351. {
  1352. opParameters[(byte)ChatParameterCode.HistoryLength] = this.PrivateChatHistoryLength;
  1353. }
  1354. return this.chatPeer.SendOperation(ChatOperationCode.Authenticate, opParameters, SendOptions.SendReliable);
  1355. }
  1356. }
  1357. else
  1358. {
  1359. if (this.DebugOut >= DebugLevel.ERROR)
  1360. {
  1361. this.listener.DebugReturn(DebugLevel.ERROR, "Can't authenticate on front end server. Authentication Values are not set");
  1362. }
  1363. return false;
  1364. }
  1365. }
  1366. private void HandleUserUnsubscribedEvent(EventData eventData)
  1367. {
  1368. string channelName = eventData.Parameters[ChatParameterCode.Channel] as string;
  1369. string userId = eventData.Parameters[ChatParameterCode.UserId] as string;
  1370. ChatChannel channel;
  1371. if (this.PublicChannels.TryGetValue(channelName, out channel))
  1372. {
  1373. if (!channel.PublishSubscribers)
  1374. {
  1375. if (this.DebugOut >= DebugLevel.WARNING)
  1376. {
  1377. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" for incoming UserUnsubscribed (\"{1}\") event does not have PublishSubscribers enabled.", channelName, userId));
  1378. }
  1379. }
  1380. if (!channel.RemoveSubscriber(userId)) // user not found!
  1381. {
  1382. if (this.DebugOut >= DebugLevel.WARNING)
  1383. {
  1384. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" does not contain unsubscribed user \"{1}\".", channelName, userId));
  1385. }
  1386. }
  1387. }
  1388. else
  1389. {
  1390. if (this.DebugOut >= DebugLevel.WARNING)
  1391. {
  1392. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" not found for incoming UserUnsubscribed (\"{1}\") event.", channelName, userId));
  1393. }
  1394. }
  1395. this.listener.OnUserUnsubscribed(channelName, userId);
  1396. }
  1397. private void HandleUserSubscribedEvent(EventData eventData)
  1398. {
  1399. string channelName = eventData.Parameters[ChatParameterCode.Channel] as string;
  1400. string userId = eventData.Parameters[ChatParameterCode.UserId] as string;
  1401. ChatChannel channel;
  1402. if (this.PublicChannels.TryGetValue(channelName, out channel))
  1403. {
  1404. if (!channel.PublishSubscribers)
  1405. {
  1406. if (this.DebugOut >= DebugLevel.WARNING)
  1407. {
  1408. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" for incoming UserSubscribed (\"{1}\") event does not have PublishSubscribers enabled.", channelName, userId));
  1409. }
  1410. }
  1411. if (!channel.AddSubscriber(userId)) // user came back from the dead ?
  1412. {
  1413. if (this.DebugOut >= DebugLevel.WARNING)
  1414. {
  1415. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" already contains newly subscribed user \"{1}\".", channelName, userId));
  1416. }
  1417. }
  1418. else if (channel.MaxSubscribers > 0 && channel.Subscribers.Count > channel.MaxSubscribers)
  1419. {
  1420. if (this.DebugOut >= DebugLevel.WARNING)
  1421. {
  1422. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\"'s MaxSubscribers exceeded. count={1} > MaxSubscribers={2}.", channelName, channel.Subscribers.Count, channel.MaxSubscribers));
  1423. }
  1424. }
  1425. #if CHAT_EXTENDED
  1426. object temp;
  1427. if (eventData.Parameters.TryGetValue(ChatParameterCode.UserProperties, out temp))
  1428. {
  1429. Dictionary<object, object> userProperties = temp as Dictionary<object, object>;
  1430. channel.ReadUserProperties(userId, userProperties);
  1431. }
  1432. #endif
  1433. }
  1434. else
  1435. {
  1436. if (this.DebugOut >= DebugLevel.WARNING)
  1437. {
  1438. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel \"{0}\" not found for incoming UserSubscribed (\"{1}\") event.", channelName, userId));
  1439. }
  1440. }
  1441. this.listener.OnUserSubscribed(channelName, userId);
  1442. }
  1443. #endregion
  1444. /// <summary>
  1445. /// Subscribe to a single channel and optionally sets its well-know channel properties in case the channel is created.
  1446. /// </summary>
  1447. /// <param name="channel">name of the channel to subscribe to</param>
  1448. /// <param name="lastMsgId">ID of the last received message from this channel when re subscribing to receive only missed messages, default is 0</param>
  1449. /// <param name="messagesFromHistory">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.</param>
  1450. /// <param name="creationOptions">options to be used in case the channel to subscribe to will be created.</param>
  1451. /// <returns></returns>
  1452. public bool Subscribe(string channel, int lastMsgId = 0, int messagesFromHistory = -1, ChannelCreationOptions creationOptions = null)
  1453. {
  1454. if (creationOptions == null)
  1455. {
  1456. creationOptions = ChannelCreationOptions.Default;
  1457. }
  1458. int maxSubscribers = creationOptions.MaxSubscribers;
  1459. bool publishSubscribers = creationOptions.PublishSubscribers;
  1460. if (maxSubscribers < 0)
  1461. {
  1462. if (this.DebugOut >= DebugLevel.ERROR)
  1463. {
  1464. this.listener.DebugReturn(DebugLevel.ERROR, "Cannot set MaxSubscribers < 0.");
  1465. }
  1466. return false;
  1467. }
  1468. if (lastMsgId < 0)
  1469. {
  1470. if (this.DebugOut >= DebugLevel.ERROR)
  1471. {
  1472. this.listener.DebugReturn(DebugLevel.ERROR, "lastMsgId cannot be < 0.");
  1473. }
  1474. return false;
  1475. }
  1476. if (messagesFromHistory < -1)
  1477. {
  1478. if (this.DebugOut >= DebugLevel.WARNING)
  1479. {
  1480. this.listener.DebugReturn(DebugLevel.WARNING, "messagesFromHistory < -1, setting it to -1");
  1481. }
  1482. messagesFromHistory = -1;
  1483. }
  1484. if (lastMsgId > 0 && messagesFromHistory == 0)
  1485. {
  1486. if (this.DebugOut >= DebugLevel.WARNING)
  1487. {
  1488. this.listener.DebugReturn(DebugLevel.WARNING, "lastMsgId will be ignored because messagesFromHistory == 0");
  1489. }
  1490. lastMsgId = 0;
  1491. }
  1492. Dictionary<object, object> properties = null;
  1493. if (publishSubscribers)
  1494. {
  1495. if (maxSubscribers > DefaultMaxSubscribers)
  1496. {
  1497. if (this.DebugOut >= DebugLevel.ERROR)
  1498. {
  1499. this.listener.DebugReturn(DebugLevel.ERROR,
  1500. string.Format("Cannot set MaxSubscribers > {0} when PublishSubscribers == true.", DefaultMaxSubscribers));
  1501. }
  1502. return false;
  1503. }
  1504. properties = new Dictionary<object, object>();
  1505. properties[ChannelWellKnownProperties.PublishSubscribers] = true;
  1506. }
  1507. if (maxSubscribers > 0)
  1508. {
  1509. if (properties == null)
  1510. {
  1511. properties = new Dictionary<object, object>();
  1512. }
  1513. properties[ChannelWellKnownProperties.MaxSubscribers] = maxSubscribers;
  1514. }
  1515. #if CHAT_EXTENDED
  1516. if (creationOptions.CustomProperties != null && creationOptions.CustomProperties.Count > 0)
  1517. {
  1518. foreach (var pair in creationOptions.CustomProperties)
  1519. {
  1520. properties.Add(pair.Key, pair.Value);
  1521. }
  1522. }
  1523. #endif
  1524. Dictionary<byte, object> opParameters = new Dictionary<byte, object> { { ChatParameterCode.Channels, new[] { channel } } };
  1525. if (messagesFromHistory != 0)
  1526. {
  1527. opParameters.Add(ChatParameterCode.HistoryLength, messagesFromHistory);
  1528. }
  1529. if (lastMsgId > 0)
  1530. {
  1531. opParameters.Add(ChatParameterCode.MsgIds, new[] { lastMsgId });
  1532. }
  1533. if (properties != null && properties.Count > 0)
  1534. {
  1535. opParameters.Add(ChatParameterCode.Properties, properties);
  1536. }
  1537. return this.chatPeer.SendOperation(ChatOperationCode.Subscribe, opParameters, SendOptions.SendReliable);
  1538. }
  1539. #if CHAT_EXTENDED
  1540. internal bool SetChannelProperties(string channelName, Dictionary<object, object> channelProperties, Dictionary<object, object> expectedProperties = null, bool httpForward = false)
  1541. {
  1542. if (!this.CanChat)
  1543. {
  1544. this.listener.DebugReturn(DebugLevel.ERROR, "SetChannelProperties called while not connected to front end server.");
  1545. return false;
  1546. }
  1547. if (string.IsNullOrEmpty(channelName) || channelProperties == null || channelProperties.Count == 0)
  1548. {
  1549. this.listener.DebugReturn(DebugLevel.WARNING, "SetChannelProperties parameters must be non-null and not empty.");
  1550. return false;
  1551. }
  1552. Dictionary<byte, object> parameters = new Dictionary<byte, object>
  1553. {
  1554. { ChatParameterCode.Channel, channelName },
  1555. { ChatParameterCode.Properties, channelProperties },
  1556. { ChatParameterCode.Broadcast, true }
  1557. };
  1558. if (httpForward)
  1559. {
  1560. parameters.Add(ChatParameterCode.WebFlags, HttpForwardWebFlag);
  1561. }
  1562. if (expectedProperties != null && expectedProperties.Count > 0)
  1563. {
  1564. parameters.Add(ChatParameterCode.ExpectedValues, expectedProperties);
  1565. }
  1566. return this.chatPeer.SendOperation(ChatOperationCode.SetProperties, parameters, SendOptions.SendReliable);
  1567. }
  1568. public bool SetCustomChannelProperties(string channelName, Dictionary<string, object> channelProperties, Dictionary<string, object> expectedProperties = null, bool httpForward = false)
  1569. {
  1570. if (channelProperties != null && channelProperties.Count > 0)
  1571. {
  1572. Dictionary<object, object> properties = new Dictionary<object, object>(channelProperties.Count);
  1573. foreach (var pair in channelProperties)
  1574. {
  1575. properties.Add(pair.Key, pair.Value);
  1576. }
  1577. Dictionary<object, object> expected = null;
  1578. if (expectedProperties != null && expectedProperties.Count > 0)
  1579. {
  1580. expected = new Dictionary<object, object>(expectedProperties.Count);
  1581. foreach (var pair in expectedProperties)
  1582. {
  1583. expected.Add(pair.Key, pair.Value);
  1584. }
  1585. }
  1586. return this.SetChannelProperties(channelName, properties, expected, httpForward);
  1587. }
  1588. return this.SetChannelProperties(channelName, null);
  1589. }
  1590. public bool SetCustomUserProperties(string channelName, string userId, Dictionary<string, object> userProperties, Dictionary<string, object> expectedProperties = null, bool httpForward = false)
  1591. {
  1592. if (userProperties != null && userProperties.Count > 0)
  1593. {
  1594. Dictionary<object, object> properties = new Dictionary<object, object>(userProperties.Count);
  1595. foreach (var pair in userProperties)
  1596. {
  1597. properties.Add(pair.Key, pair.Value);
  1598. }
  1599. Dictionary<object, object> expected = null;
  1600. if (expectedProperties != null && expectedProperties.Count > 0)
  1601. {
  1602. expected = new Dictionary<object, object>(expectedProperties.Count);
  1603. foreach (var pair in expectedProperties)
  1604. {
  1605. expected.Add(pair.Key, pair.Value);
  1606. }
  1607. }
  1608. return this.SetUserProperties(channelName, userId, properties, expected, httpForward);
  1609. }
  1610. return this.SetUserProperties(channelName, userId, null);
  1611. }
  1612. internal bool SetUserProperties(string channelName, string userId, Dictionary<object, object> channelProperties, Dictionary<object, object> expectedProperties = null, bool httpForward = false)
  1613. {
  1614. if (!this.CanChat)
  1615. {
  1616. this.listener.DebugReturn(DebugLevel.ERROR, "SetUserProperties called while not connected to front end server.");
  1617. return false;
  1618. }
  1619. if (string.IsNullOrEmpty(channelName))
  1620. {
  1621. this.listener.DebugReturn(DebugLevel.WARNING, "SetUserProperties \"channelName\" parameter must be non-null and not empty.");
  1622. return false;
  1623. }
  1624. if (channelProperties == null || channelProperties.Count == 0)
  1625. {
  1626. this.listener.DebugReturn(DebugLevel.WARNING, "SetUserProperties \"channelProperties\" parameter must be non-null and not empty.");
  1627. return false;
  1628. }
  1629. if (string.IsNullOrEmpty(userId))
  1630. {
  1631. this.listener.DebugReturn(DebugLevel.WARNING, "SetUserProperties \"userId\" parameter must be non-null and not empty.");
  1632. return false;
  1633. }
  1634. Dictionary<byte, object> parameters = new Dictionary<byte, object>
  1635. {
  1636. { ChatParameterCode.Channel, channelName },
  1637. { ChatParameterCode.Properties, channelProperties },
  1638. { ChatParameterCode.UserId, userId },
  1639. { ChatParameterCode.Broadcast, true }
  1640. };
  1641. if (httpForward)
  1642. {
  1643. parameters.Add(ChatParameterCode.WebFlags, HttpForwardWebFlag);
  1644. }
  1645. if (expectedProperties != null && expectedProperties.Count > 0)
  1646. {
  1647. parameters.Add(ChatParameterCode.ExpectedValues, expectedProperties);
  1648. }
  1649. return this.chatPeer.SendOperation(ChatOperationCode.SetProperties, parameters, SendOptions.SendReliable);
  1650. }
  1651. private void HandlePropertiesChanged(EventData eventData)
  1652. {
  1653. string channelName = eventData.Parameters[ChatParameterCode.Channel] as string;
  1654. ChatChannel channel;
  1655. if (!this.PublicChannels.TryGetValue(channelName, out channel))
  1656. {
  1657. this.listener.DebugReturn(DebugLevel.WARNING, string.Format("Channel {0} for incoming ChannelPropertiesUpdated event not found.", channelName));
  1658. return;
  1659. }
  1660. string senderId = eventData.Parameters[ChatParameterCode.Sender] as string;
  1661. Dictionary<object, object> changedProperties = eventData.Parameters[ChatParameterCode.Properties] as Dictionary<object, object>;
  1662. object temp;
  1663. if (eventData.Parameters.TryGetValue(ChatParameterCode.UserId, out temp))
  1664. {
  1665. string targetUserId = temp as string;
  1666. channel.ReadUserProperties(targetUserId, changedProperties);
  1667. this.listener.OnUserPropertiesChanged(channelName, targetUserId, senderId, changedProperties);
  1668. }
  1669. else
  1670. {
  1671. channel.ReadChannelProperties(changedProperties);
  1672. this.listener.OnChannelPropertiesChanged(channelName, senderId, changedProperties);
  1673. }
  1674. }
  1675. private void HandleErrorInfoEvent(EventData eventData)
  1676. {
  1677. string channel = eventData.Parameters[ChatParameterCode.Channel] as string;
  1678. string msg = eventData.Parameters[ChatParameterCode.DebugMessage] as string;
  1679. object data = eventData.Parameters[ChatParameterCode.DebugData];
  1680. this.listener.OnErrorInfo(channel, msg, data);
  1681. }
  1682. #endif
  1683. }
  1684. }