Repository: heroiclabs/nakama-dotnet Branch: master Commit: 6a51c8fd90a7 Files: 1862 Total size: 12.8 MB Directory structure: gitextract_98wg7jk9/ ├── .editorconfig ├── .github/ │ └── workflows/ │ ├── doxygen.yml │ └── pr.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Nakama/ │ ├── ApiClient.gen.cs │ ├── ChannelJoinMessage.cs │ ├── ChannelLeaveMessage.cs │ ├── ChannelRemoveMessage.cs │ ├── ChannelSendMessage.cs │ ├── ChannelUpdateMessage.cs │ ├── Client.cs │ ├── Console/ │ │ └── ConsoleClient.gen.cs │ ├── GZipHttpClientHandler.cs │ ├── HttpRequestAdapter.cs │ ├── IChannel.cs │ ├── IChannelMessageAck.cs │ ├── IChannelPresenceEvent.cs │ ├── IClient.cs │ ├── IHttpAdapter.cs │ ├── IHttpAdapterUtil.cs │ ├── ILogger.cs │ ├── IMatch.cs │ ├── IMatchPresenceEvent.cs │ ├── IMatchState.cs │ ├── IMatchmakerMatched.cs │ ├── IMatchmakerTicket.cs │ ├── IParty.cs │ ├── IPartyClose.cs │ ├── IPartyData.cs │ ├── IPartyJoinRequest.cs │ ├── IPartyLeader.cs │ ├── IPartyMatchmakerTicket.cs │ ├── IPartyPresenceEvent.cs │ ├── IPartyUpdate.cs │ ├── ISession.cs │ ├── ISocket.cs │ ├── ISocketAdapter.cs │ ├── IStatus.cs │ ├── IStatusPresenceEvent.cs │ ├── IStreamPresenceEvent.cs │ ├── IUserPresence.cs │ ├── MatchCreateMessage.cs │ ├── MatchJoinMessage.cs │ ├── MatchLeaveMessage.cs │ ├── MatchSendMessage.cs │ ├── MatchmakerAddMessage.cs │ ├── MatchmakerRemoveMessage.cs │ ├── Nakama.csproj │ ├── Ninja.WebSockets/ │ │ ├── BufferPool.cs │ │ ├── Exceptions/ │ │ │ ├── EntityTooLargeException.cs │ │ │ ├── InvalidHttpResponseCodeException.cs │ │ │ ├── README.txt │ │ │ ├── SecWebSocketKeyMissingException.cs │ │ │ ├── ServerListenerSocketException.cs │ │ │ ├── WebSocketBufferOverflowException.cs │ │ │ ├── WebSocketHandshakeFailedException.cs │ │ │ └── WebSocketVersionNotSupportedException.cs │ │ ├── HttpHelper.cs │ │ ├── IBufferPool.cs │ │ ├── IPingPongManager.cs │ │ ├── IWebSocketClientFactory.cs │ │ ├── IWebSocketServerFactory.cs │ │ ├── Internal/ │ │ │ ├── BinaryReaderWriter.cs │ │ │ ├── WebSocketFrame.cs │ │ │ ├── WebSocketFrameCommon.cs │ │ │ ├── WebSocketFrameReader.cs │ │ │ ├── WebSocketFrameWriter.cs │ │ │ ├── WebSocketImplementation.cs │ │ │ ├── WebSocketOpCode.cs │ │ │ └── WebSocketReadCursor.cs │ │ ├── LICENCE │ │ ├── PingPongManager.cs │ │ ├── PongEventArgs.cs │ │ ├── WebSocketClientFactory.cs │ │ ├── WebSocketClientOptions.cs │ │ ├── WebSocketHttpContext.cs │ │ ├── WebSocketServerFactory.cs │ │ └── WebSocketServerOptions.cs │ ├── NullLogger.cs │ ├── Party.cs │ ├── PartyAccept.cs │ ├── PartyClose.cs │ ├── PartyCreate.cs │ ├── PartyData.cs │ ├── PartyDataSend.cs │ ├── PartyJoin.cs │ ├── PartyJoinRequest.cs │ ├── PartyJoinRequestList.cs │ ├── PartyLeader.cs │ ├── PartyLeave.cs │ ├── PartyMatchmakerAdd.cs │ ├── PartyMatchmakerRemove.cs │ ├── PartyMatchmakerTicket.cs │ ├── PartyMemberRemove.cs │ ├── PartyPresenceEvent.cs │ ├── PartyPromote.cs │ ├── PartyUpdate.cs │ ├── PresenceUtil.cs │ ├── PreserveAttribute.cs │ ├── Retry.cs │ ├── RetryConfiguration.cs │ ├── RetryHistory.cs │ ├── RetryInvoker.cs │ ├── RetryJitter.cs │ ├── RetryListener.cs │ ├── Session.cs │ ├── Socket.cs │ ├── StatusFollowMessage.cs │ ├── StatusUnfollowMessage.cs │ ├── StatusUpdateMessage.cs │ ├── StorageObjectId.cs │ ├── TinyJson/ │ │ ├── JsonParser.cs │ │ ├── JsonWriter.cs │ │ └── LICENSE │ ├── TransientExceptionDelegate.cs │ ├── WebSocketAdapter.cs │ ├── WebSocketErrorMessage.cs │ ├── WebSocketMessageEnvelope.cs │ ├── WebSocketStdlibAdapter.cs │ └── WriteStorageObject.cs ├── Nakama.Tests/ │ ├── AssemblyInfo.cs │ ├── AuthenticateTest.cs │ ├── AwaitedSocketTaskTest.cs │ ├── CancelTest.cs │ ├── FriendTest.cs │ ├── GroupTest.cs │ ├── HttpErrorTest.cs │ ├── LeaderboardAroundOwnerTest.cs │ ├── LeaderboardTest.cs │ ├── LinkUnlinkTest.cs │ ├── Nakama.Tests.csproj │ ├── PresenceUtilTest.cs │ ├── RetryTest.cs │ ├── RpcTest.cs │ ├── SessionTest.cs │ ├── Socket/ │ │ ├── WebSocketChannelTest.cs │ │ ├── WebSocketMatchTest.cs │ │ ├── WebSocketMatchmakerTest.cs │ │ ├── WebSocketNotificationTest.cs │ │ ├── WebSocketPartyTest.cs │ │ ├── WebSocketRpcTest.cs │ │ ├── WebSocketTest.cs │ │ └── WebSocketUserStatusTest.cs │ ├── StdoutLogger.cs │ ├── TestsUtil.cs │ ├── TinyJsonParserTest.cs │ ├── TransientExceptionHttpAdapter.cs │ └── settings.json ├── Nakama.sln ├── README.md ├── RELEASEINST.md ├── Satori/ │ ├── ApiClient.gen.cs │ ├── Client.cs │ ├── Console/ │ │ └── ConsoleClient.gen.cs │ ├── Event.cs │ ├── GZipHttpClientHandler.cs │ ├── HttpRequestAdapter.cs │ ├── IClient.cs │ ├── IHttpAdapter.cs │ ├── IHttpAdapterUtil.cs │ ├── ILogger.cs │ ├── ISession.cs │ ├── NullLogger.cs │ ├── PreserveAttribute.cs │ ├── Retry.cs │ ├── RetryConfiguration.cs │ ├── RetryHistory.cs │ ├── RetryInvoker.cs │ ├── RetryJitter.cs │ ├── RetryListener.cs │ ├── Satori.csproj │ ├── Session.cs │ ├── TinyJson/ │ │ ├── JsonParser.cs │ │ ├── JsonWriter.cs │ │ └── LICENSE │ └── TransientExceptionDelegate.cs ├── Satori.Tests/ │ ├── ClientIdentifyTest.cs │ ├── ClientTest.cs │ ├── README.md │ └── Satori.Tests.csproj ├── Taskfile.dist.yml ├── codegen/ │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go └── docs/ ├── .nojekyll ├── CNAME ├── Doxyfile ├── html/ │ ├── _api_client_8gen_8cs.html │ ├── _api_client_8gen_8cs.js │ ├── _binary_reader_writer_8cs.html │ ├── _buffer_pool_8cs.html │ ├── _buffer_pool_8cs.js │ ├── _c_h_a_n_g_e_l_o_g_8md.html │ ├── _channel_join_message_8cs.html │ ├── _channel_join_message_8cs.js │ ├── _channel_leave_message_8cs.html │ ├── _channel_remove_message_8cs.html │ ├── _channel_send_message_8cs.html │ ├── _channel_update_message_8cs.html │ ├── _client_8cs.html │ ├── _client_8cs.js │ ├── _console_client_8gen_8cs.html │ ├── _console_client_8gen_8cs.js │ ├── _debug_2net46_2_8_n_e_t_framework_00_version_0av4_86_8_assembly_attributes_8cs.html │ ├── _debug_2net46_2_nakama_8_assembly_info_8cs.html │ ├── _debug_2net46_2_satori_8_assembly_info_8cs.html │ ├── _debug_2netstandard2_80_2_8_n_e_t_standard_00_version_0av2_80_8_assembly_attributes_8cs.html │ ├── _debug_2netstandard2_80_2_nakama_8_assembly_info_8cs.html │ ├── _debug_2netstandard2_80_2_satori_8_assembly_info_8cs.html │ ├── _debug_2netstandard2_81_2_nakama_8_assembly_info_8cs.html │ ├── _debug_2netstandard2_81_2_satori_8_assembly_info_8cs.html │ ├── _entity_too_large_exception_8cs.html │ ├── _entity_too_large_exception_8cs.js │ ├── _event_8cs.html │ ├── _event_8cs.js │ ├── _g_zip_http_client_handler_8cs.html │ ├── _http_helper_8cs.html │ ├── _http_helper_8cs.js │ ├── _http_request_adapter_8cs.html │ ├── _http_request_adapter_8cs.js │ ├── _i_buffer_pool_8cs.html │ ├── _i_buffer_pool_8cs.js │ ├── _i_channel_8cs.html │ ├── _i_channel_8cs.js │ ├── _i_channel_message_ack_8cs.html │ ├── _i_channel_message_ack_8cs.js │ ├── _i_channel_presence_event_8cs.html │ ├── _i_channel_presence_event_8cs.js │ ├── _i_client_8cs.html │ ├── _i_client_8cs.js │ ├── _i_http_adapter_8cs.html │ ├── _i_http_adapter_8cs.js │ ├── _i_http_adapter_util_8cs.html │ ├── _i_logger_8cs.html │ ├── _i_logger_8cs.js │ ├── _i_match_8cs.html │ ├── _i_match_8cs.js │ ├── _i_match_presence_event_8cs.html │ ├── _i_match_presence_event_8cs.js │ ├── _i_match_state_8cs.html │ ├── _i_match_state_8cs.js │ ├── _i_matchmaker_matched_8cs.html │ ├── _i_matchmaker_matched_8cs.js │ ├── _i_matchmaker_ticket_8cs.html │ ├── _i_matchmaker_ticket_8cs.js │ ├── _i_party_8cs.html │ ├── _i_party_8cs.js │ ├── _i_party_close_8cs.html │ ├── _i_party_close_8cs.js │ ├── _i_party_data_8cs.html │ ├── _i_party_data_8cs.js │ ├── _i_party_join_request_8cs.html │ ├── _i_party_join_request_8cs.js │ ├── _i_party_leader_8cs.html │ ├── _i_party_leader_8cs.js │ ├── _i_party_matchmaker_ticket_8cs.html │ ├── _i_party_matchmaker_ticket_8cs.js │ ├── _i_party_presence_event_8cs.html │ ├── _i_party_presence_event_8cs.js │ ├── _i_party_update_8cs.html │ ├── _i_party_update_8cs.js │ ├── _i_ping_pong_manager_8cs.html │ ├── _i_ping_pong_manager_8cs.js │ ├── _i_session_8cs.html │ ├── _i_session_8cs.js │ ├── _i_socket_8cs.html │ ├── _i_socket_8cs.js │ ├── _i_socket_adapter_8cs.html │ ├── _i_socket_adapter_8cs.js │ ├── _i_status_8cs.html │ ├── _i_status_8cs.js │ ├── _i_status_presence_event_8cs.html │ ├── _i_status_presence_event_8cs.js │ ├── _i_stream_presence_event_8cs.html │ ├── _i_stream_presence_event_8cs.js │ ├── _i_user_presence_8cs.html │ ├── _i_user_presence_8cs.js │ ├── _i_web_socket_client_factory_8cs.html │ ├── _i_web_socket_client_factory_8cs.js │ ├── _i_web_socket_server_factory_8cs.html │ ├── _i_web_socket_server_factory_8cs.js │ ├── _invalid_http_response_code_exception_8cs.html │ ├── _invalid_http_response_code_exception_8cs.js │ ├── _json_parser_8cs.html │ ├── _json_writer_8cs.html │ ├── _match_create_message_8cs.html │ ├── _match_join_message_8cs.html │ ├── _match_leave_message_8cs.html │ ├── _match_send_message_8cs.html │ ├── _matchmaker_add_message_8cs.html │ ├── _matchmaker_remove_message_8cs.html │ ├── _nakama_2_api_client_8gen_8cs.html │ ├── _nakama_2_api_client_8gen_8cs.js │ ├── _nakama_2_client_8cs.html │ ├── _nakama_2_client_8cs.js │ ├── _nakama_2_g_zip_http_client_handler_8cs.html │ ├── _nakama_2_http_request_adapter_8cs.html │ ├── _nakama_2_http_request_adapter_8cs.js │ ├── _nakama_2_i_client_8cs.html │ ├── _nakama_2_i_client_8cs.js │ ├── _nakama_2_i_http_adapter_8cs.html │ ├── _nakama_2_i_http_adapter_8cs.js │ ├── _nakama_2_i_http_adapter_util_8cs.html │ ├── _nakama_2_i_logger_8cs.html │ ├── _nakama_2_i_logger_8cs.js │ ├── _nakama_2_i_session_8cs.html │ ├── _nakama_2_i_session_8cs.js │ ├── _nakama_2_preserve_attribute_8cs.html │ ├── _nakama_2_retry_8cs.html │ ├── _nakama_2_retry_8cs.js │ ├── _nakama_2_retry_configuration_8cs.html │ ├── _nakama_2_retry_configuration_8cs.js │ ├── _nakama_2_retry_history_8cs.html │ ├── _nakama_2_retry_invoker_8cs.html │ ├── _nakama_2_retry_jitter_8cs.html │ ├── _nakama_2_retry_jitter_8cs.js │ ├── _nakama_2_retry_listener_8cs.html │ ├── _nakama_2_retry_listener_8cs.js │ ├── _nakama_2_session_8cs.html │ ├── _nakama_2_session_8cs.js │ ├── _nakama_2_tiny_json_2_json_parser_8cs.html │ ├── _nakama_2_tiny_json_2_json_writer_8cs.html │ ├── _nakama_2_transient_exception_delegate_8cs.html │ ├── _nakama_2_transient_exception_delegate_8cs.js │ ├── _nakama_2obj_2_debug_2net46_2_8_n_e_t_framework_00_version_0av4_86_8_assembly_attributes_8cs.html │ ├── _nakama_2obj_2_debug_2netstandard2_80_2_8_n_e_t_standard_00_version_0av2_80_8_assembly_attributes_8cs.html │ ├── _nakama_2obj_2_debug_2netstandard2_81_2_8_n_e_t_standard_00_version_0av2_81_8_assembly_attributes_8cs.html │ ├── _nakama_2obj_2_release_2net46_2_8_n_e_t_framework_00_version_0av4_86_8_assembly_attributes_8cs.html │ ├── _nakama_2obj_2_release_2netstandard2_80_2_8_n_e_t_standard_00_version_0av2_80_8_assembly_attributes_8cs.html │ ├── _nakama_2obj_2_release_2netstandard2_81_2_8_n_e_t_standard_00_version_0av2_81_8_assembly_attributes_8cs.html │ ├── _null_logger_8cs.html │ ├── _party_8cs.html │ ├── _party_accept_8cs.html │ ├── _party_close_8cs.html │ ├── _party_create_8cs.html │ ├── _party_data_8cs.html │ ├── _party_data_send_8cs.html │ ├── _party_join_8cs.html │ ├── _party_join_request_8cs.html │ ├── _party_join_request_list_8cs.html │ ├── _party_leader_8cs.html │ ├── _party_leave_8cs.html │ ├── _party_matchmaker_add_8cs.html │ ├── _party_matchmaker_remove_8cs.html │ ├── _party_matchmaker_ticket_8cs.html │ ├── _party_member_remove_8cs.html │ ├── _party_presence_event_8cs.html │ ├── _party_promote_8cs.html │ ├── _party_update_8cs.html │ ├── _ping_pong_manager_8cs.html │ ├── _ping_pong_manager_8cs.js │ ├── _pong_event_args_8cs.html │ ├── _pong_event_args_8cs.js │ ├── _presence_util_8cs.html │ ├── _preserve_attribute_8cs.html │ ├── _r_e_a_d_m_e_8md.html │ ├── _release_2net46_2_8_n_e_t_framework_00_version_0av4_86_8_assembly_attributes_8cs.html │ ├── _release_2net46_2_nakama_8_assembly_info_8cs.html │ ├── _release_2net46_2_satori_8_assembly_info_8cs.html │ ├── _release_2netstandard2_80_2_8_n_e_t_standard_00_version_0av2_80_8_assembly_attributes_8cs.html │ ├── _release_2netstandard2_80_2_nakama_8_assembly_info_8cs.html │ ├── _release_2netstandard2_80_2_satori_8_assembly_info_8cs.html │ ├── _release_2netstandard2_81_2_nakama_8_assembly_info_8cs.html │ ├── _release_2netstandard2_81_2_satori_8_assembly_info_8cs.html │ ├── _retry_8cs.html │ ├── _retry_8cs.js │ ├── _retry_configuration_8cs.html │ ├── _retry_configuration_8cs.js │ ├── _retry_history_8cs.html │ ├── _retry_invoker_8cs.html │ ├── _retry_jitter_8cs.html │ ├── _retry_jitter_8cs.js │ ├── _retry_listener_8cs.html │ ├── _retry_listener_8cs.js │ ├── _satori_2_api_client_8gen_8cs.html │ ├── _satori_2_api_client_8gen_8cs.js │ ├── _satori_2_client_8cs.html │ ├── _satori_2_client_8cs.js │ ├── _satori_2_g_zip_http_client_handler_8cs.html │ ├── _satori_2_http_request_adapter_8cs.html │ ├── _satori_2_http_request_adapter_8cs.js │ ├── _satori_2_i_client_8cs.html │ ├── _satori_2_i_client_8cs.js │ ├── _satori_2_i_http_adapter_8cs.html │ ├── _satori_2_i_http_adapter_8cs.js │ ├── _satori_2_i_http_adapter_util_8cs.html │ ├── _satori_2_i_logger_8cs.html │ ├── _satori_2_i_logger_8cs.js │ ├── _satori_2_i_session_8cs.html │ ├── _satori_2_i_session_8cs.js │ ├── _satori_2_preserve_attribute_8cs.html │ ├── _satori_2_retry_8cs.html │ ├── _satori_2_retry_8cs.js │ ├── _satori_2_retry_configuration_8cs.html │ ├── _satori_2_retry_configuration_8cs.js │ ├── _satori_2_retry_history_8cs.html │ ├── _satori_2_retry_invoker_8cs.html │ ├── _satori_2_retry_jitter_8cs.html │ ├── _satori_2_retry_jitter_8cs.js │ ├── _satori_2_retry_listener_8cs.html │ ├── _satori_2_retry_listener_8cs.js │ ├── _satori_2_session_8cs.html │ ├── _satori_2_session_8cs.js │ ├── _satori_2_tiny_json_2_json_parser_8cs.html │ ├── _satori_2_tiny_json_2_json_writer_8cs.html │ ├── _satori_2_transient_exception_delegate_8cs.html │ ├── _satori_2_transient_exception_delegate_8cs.js │ ├── _satori_2obj_2_debug_2net46_2_8_n_e_t_framework_00_version_0av4_86_8_assembly_attributes_8cs.html │ ├── _satori_2obj_2_debug_2netstandard2_80_2_8_n_e_t_standard_00_version_0av2_80_8_assembly_attributes_8cs.html │ ├── _satori_2obj_2_debug_2netstandard2_81_2_8_n_e_t_standard_00_version_0av2_81_8_assembly_attributes_8cs.html │ ├── _satori_2obj_2_release_2net46_2_8_n_e_t_framework_00_version_0av4_86_8_assembly_attributes_8cs.html │ ├── _satori_2obj_2_release_2netstandard2_80_2_8_n_e_t_standard_00_version_0av2_80_8_assembly_attributes_8cs.html │ ├── _satori_2obj_2_release_2netstandard2_81_2_8_n_e_t_standard_00_version_0av2_81_8_assembly_attributes_8cs.html │ ├── _sec_web_socket_key_missing_exception_8cs.html │ ├── _sec_web_socket_key_missing_exception_8cs.js │ ├── _server_listener_socket_exception_8cs.html │ ├── _server_listener_socket_exception_8cs.js │ ├── _session_8cs.html │ ├── _session_8cs.js │ ├── _socket_8cs.html │ ├── _socket_8cs.js │ ├── _status_follow_message_8cs.html │ ├── _status_unfollow_message_8cs.html │ ├── _status_update_message_8cs.html │ ├── _storage_object_id_8cs.html │ ├── _storage_object_id_8cs.js │ ├── _transient_exception_delegate_8cs.html │ ├── _transient_exception_delegate_8cs.js │ ├── _web_socket_adapter_8cs.html │ ├── _web_socket_adapter_8cs.js │ ├── _web_socket_buffer_overflow_exception_8cs.html │ ├── _web_socket_buffer_overflow_exception_8cs.js │ ├── _web_socket_client_factory_8cs.html │ ├── _web_socket_client_factory_8cs.js │ ├── _web_socket_client_options_8cs.html │ ├── _web_socket_client_options_8cs.js │ ├── _web_socket_error_message_8cs.html │ ├── _web_socket_frame_8cs.html │ ├── _web_socket_frame_common_8cs.html │ ├── _web_socket_frame_reader_8cs.html │ ├── _web_socket_frame_writer_8cs.html │ ├── _web_socket_handshake_failed_exception_8cs.html │ ├── _web_socket_handshake_failed_exception_8cs.js │ ├── _web_socket_http_context_8cs.html │ ├── _web_socket_http_context_8cs.js │ ├── _web_socket_implementation_8cs.html │ ├── _web_socket_message_envelope_8cs.html │ ├── _web_socket_op_code_8cs.html │ ├── _web_socket_read_cursor_8cs.html │ ├── _web_socket_server_factory_8cs.html │ ├── _web_socket_server_factory_8cs.js │ ├── _web_socket_server_options_8cs.html │ ├── _web_socket_server_options_8cs.js │ ├── _web_socket_stdlib_adapter_8cs.html │ ├── _web_socket_stdlib_adapter_8cs.js │ ├── _web_socket_version_not_supported_exception_8cs.html │ ├── _web_socket_version_not_supported_exception_8cs.js │ ├── _write_storage_object_8cs.html │ ├── _write_storage_object_8cs.js │ ├── annotated.html │ ├── annotated_dup.js │ ├── class_nakama_1_1_api_response_exception-members.html │ ├── class_nakama_1_1_api_response_exception.html │ ├── class_nakama_1_1_api_response_exception.js │ ├── class_nakama_1_1_client-members.html │ ├── class_nakama_1_1_client.html │ ├── class_nakama_1_1_client.js │ ├── class_nakama_1_1_console_1_1_api_response_exception-members.html │ ├── class_nakama_1_1_console_1_1_api_response_exception.html │ ├── class_nakama_1_1_console_1_1_api_response_exception.js │ ├── class_nakama_1_1_http_request_adapter-members.html │ ├── class_nakama_1_1_http_request_adapter.html │ ├── class_nakama_1_1_http_request_adapter.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool_1_1_public_buffer_memory_stream-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool_1_1_public_buffer_memory_stream.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool_1_1_public_buffer_memory_stream.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_entity_too_large_exception-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_entity_too_large_exception.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_entity_too_large_exception.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_invalid_http_response_code_exception-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_invalid_http_response_code_exception.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_invalid_http_response_code_exception.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_sec_web_socket_key_missing_exception-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_sec_web_socket_key_missing_exception.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_sec_web_socket_key_missing_exception.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_server_listener_socket_exception-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_server_listener_socket_exception.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_server_listener_socket_exception.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_buffer_overflow_exception-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_buffer_overflow_exception.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_buffer_overflow_exception.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_handshake_failed_exception-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_handshake_failed_exception.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_handshake_failed_exception.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_version_not_supported_exception-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_version_not_supported_exception.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_version_not_supported_exception.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_http_helper-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_http_helper.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_ping_pong_manager-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_ping_pong_manager.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_ping_pong_manager.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_pong_event_args-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_pong_event_args.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_pong_event_args.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_client_factory-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_client_factory.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_client_factory.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_client_options-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_client_options.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_client_options.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_http_context-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_http_context.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_http_context.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_server_factory-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_server_factory.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_server_factory.js │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_server_options-members.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_server_options.html │ ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_server_options.js │ ├── class_nakama_1_1_retry-members.html │ ├── class_nakama_1_1_retry.html │ ├── class_nakama_1_1_retry.js │ ├── class_nakama_1_1_retry_configuration-members.html │ ├── class_nakama_1_1_retry_configuration.html │ ├── class_nakama_1_1_retry_configuration.js │ ├── class_nakama_1_1_session-members.html │ ├── class_nakama_1_1_session.html │ ├── class_nakama_1_1_session.js │ ├── class_nakama_1_1_socket-members.html │ ├── class_nakama_1_1_socket.html │ ├── class_nakama_1_1_socket.js │ ├── class_nakama_1_1_storage_object_id-members.html │ ├── class_nakama_1_1_storage_object_id.html │ ├── class_nakama_1_1_storage_object_id.js │ ├── class_nakama_1_1_tests_1_1_api_1_1_authenticate_test-members.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_authenticate_test.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_group_test-members.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_group_test.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_http_error_test-members.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_http_error_test.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_leaderboard_around_owner_test-members.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_leaderboard_around_owner_test.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_leaderboard_test-members.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_leaderboard_test.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_link_unlink_test-members.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_link_unlink_test.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_rpc_test-members.html │ ├── class_nakama_1_1_tests_1_1_api_1_1_rpc_test.html │ ├── class_nakama_1_1_tests_1_1_awaited_socket_task_test-members.html │ ├── class_nakama_1_1_tests_1_1_awaited_socket_task_test.html │ ├── class_nakama_1_1_tests_1_1_cancel_test-members.html │ ├── class_nakama_1_1_tests_1_1_cancel_test.html │ ├── class_nakama_1_1_tests_1_1_retry_test-members.html │ ├── class_nakama_1_1_tests_1_1_retry_test.html │ ├── class_nakama_1_1_tests_1_1_session_test-members.html │ ├── class_nakama_1_1_tests_1_1_session_test.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_channel_test-members.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_channel_test.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_match_test-members.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_match_test.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_matchmaker_test-members.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_matchmaker_test.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_notification_test-members.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_notification_test.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_party_test-members.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_party_test.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_rpc_test-members.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_rpc_test.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_test-members.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_test.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_user_status_test-members.html │ ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_user_status_test.html │ ├── class_nakama_1_1_tests_1_1_stdout_logger-members.html │ ├── class_nakama_1_1_tests_1_1_stdout_logger.html │ ├── class_nakama_1_1_tests_1_1_stdout_logger.js │ ├── class_nakama_1_1_tests_1_1_tiny_json_parser_test-members.html │ ├── class_nakama_1_1_tests_1_1_tiny_json_parser_test.html │ ├── class_nakama_1_1_tests_1_1_transient_exception_http_adapter-members.html │ ├── class_nakama_1_1_tests_1_1_transient_exception_http_adapter.html │ ├── class_nakama_1_1_tests_1_1_transient_exception_http_adapter.js │ ├── class_nakama_1_1_web_socket_adapter-members.html │ ├── class_nakama_1_1_web_socket_adapter.html │ ├── class_nakama_1_1_web_socket_adapter.js │ ├── class_nakama_1_1_web_socket_stdlib_adapter-members.html │ ├── class_nakama_1_1_web_socket_stdlib_adapter.html │ ├── class_nakama_1_1_web_socket_stdlib_adapter.js │ ├── class_nakama_1_1_write_storage_object-members.html │ ├── class_nakama_1_1_write_storage_object.html │ ├── class_nakama_1_1_write_storage_object.js │ ├── class_satori_1_1_api_response_exception-members.html │ ├── class_satori_1_1_api_response_exception.html │ ├── class_satori_1_1_api_response_exception.js │ ├── class_satori_1_1_client-members.html │ ├── class_satori_1_1_client.html │ ├── class_satori_1_1_client.js │ ├── class_satori_1_1_event-members.html │ ├── class_satori_1_1_event.html │ ├── class_satori_1_1_event.js │ ├── class_satori_1_1_http_request_adapter-members.html │ ├── class_satori_1_1_http_request_adapter.html │ ├── class_satori_1_1_http_request_adapter.js │ ├── class_satori_1_1_retry-members.html │ ├── class_satori_1_1_retry.html │ ├── class_satori_1_1_retry.js │ ├── class_satori_1_1_retry_configuration-members.html │ ├── class_satori_1_1_retry_configuration.html │ ├── class_satori_1_1_retry_configuration.js │ ├── class_satori_1_1_session-members.html │ ├── class_satori_1_1_session.html │ ├── class_satori_1_1_session.js │ ├── class_satori_1_1_tests_1_1_client_test-members.html │ ├── class_satori_1_1_tests_1_1_client_test.html │ ├── classes.html │ ├── clipboard.js │ ├── cookie.js │ ├── dir_00db1776877a30bd47a3324e3b896815.html │ ├── dir_00db1776877a30bd47a3324e3b896815.js │ ├── dir_0255d041b3ce7964bcd7b11954959c22.html │ ├── dir_0385b7cc93c13096276fd0475bf94138.html │ ├── dir_0385b7cc93c13096276fd0475bf94138.js │ ├── dir_07d4e60c212e220cb70fd11bc65ff95e.html │ ├── dir_0a71ed179ba9d4357fa1a0aa4e188f77.html │ ├── dir_0a71ed179ba9d4357fa1a0aa4e188f77.js │ ├── dir_0acaa047c55e3bbc2ca6716743379b50.html │ ├── dir_104e9d364d598921197c06c38fc2275c.html │ ├── dir_1c3008a3c461c137d9f062e2a28e5366.html │ ├── dir_1c3008a3c461c137d9f062e2a28e5366.js │ ├── dir_2024f50217af71df819eb31c540cc957.html │ ├── dir_2024f50217af71df819eb31c540cc957.js │ ├── dir_223f41b9d4a3aed2d0cd2a771bf2b672.html │ ├── dir_29bd9dc1cd33dca2d02be697ebc424d5.html │ ├── dir_29bd9dc1cd33dca2d02be697ebc424d5.js │ ├── dir_2c78f4ea1566149e6f1239d9a2bbc92d.html │ ├── dir_2c78f4ea1566149e6f1239d9a2bbc92d.js │ ├── dir_3771c35781cb72be820bcf0859828876.html │ ├── dir_40821ca9aa8b0024c09c9271c75bfc8d.html │ ├── dir_4aba2f75ac06c997db6dcdd45b346bfc.html │ ├── dir_4aba2f75ac06c997db6dcdd45b346bfc.js │ ├── dir_4d6966d1911ef40af05228884f817f01.html │ ├── dir_509efb472faf656a1f9c1c002f3dfbd0.html │ ├── dir_59425e443f801f1f2fd8bbe4959a3ccf.html │ ├── dir_5986fb63ee1c250c22ec7255d2796bed.html │ ├── dir_5986fb63ee1c250c22ec7255d2796bed.js │ ├── dir_5a43296f26836228c3ddbf8578e994aa.html │ ├── dir_5a43296f26836228c3ddbf8578e994aa.js │ ├── dir_5c537d2b32ff2d13d00336fbe6131750.html │ ├── dir_5c537d2b32ff2d13d00336fbe6131750.js │ ├── dir_5d1450713377add98c1180fe0eb2f9ae.html │ ├── dir_5d1450713377add98c1180fe0eb2f9ae.js │ ├── dir_5eb17383be0272b71916d4988c97ae3c.html │ ├── dir_5eb17383be0272b71916d4988c97ae3c.js │ ├── dir_64302e4ed8e680c5e7832e1b7ea09baa.html │ ├── dir_64302e4ed8e680c5e7832e1b7ea09baa.js │ ├── dir_68267d1309a1af8e8297ef4c3efbcdba.html │ ├── dir_6c5dd7babc86647cb00b5f49e0afc051.html │ ├── dir_6c5dd7babc86647cb00b5f49e0afc051.js │ ├── dir_717cf8a47ded45e56c3041f98a9ca441.html │ ├── dir_75202c243db9baf385df0aac94b0acc0.html │ ├── dir_75202c243db9baf385df0aac94b0acc0.js │ ├── dir_79e598b17822ac3218a31651fbd84230.html │ ├── dir_7a595d23279f99f2e5346245557e8271.html │ ├── dir_7a595d23279f99f2e5346245557e8271.js │ ├── dir_7b1b4a986f21cd07a017e6cd0f74eec4.html │ ├── dir_81ad01bee8ed91a16e2e56d92ae48530.html │ ├── dir_82823cd98c87c4bb1483bb7f879dfe68.html │ ├── dir_87bdc2ec3fb2fe36f0442c7b9fa8c83c.html │ ├── dir_87bdc2ec3fb2fe36f0442c7b9fa8c83c.js │ ├── dir_93c065d202f1b2ae4be97868117427d8.html │ ├── dir_93c065d202f1b2ae4be97868117427d8.js │ ├── dir_94394ac86fa77e96e8d4c3af72ed61db.html │ ├── dir_9ded64b83b3f5b23c7937ebd8f5ce2f1.html │ ├── dir_a159881d357f96df4223872fd74cca14.html │ ├── dir_aa00dbe797bd24fe05814e1a03a446ba.html │ ├── dir_b668d86527323370c4668cb8bf07410d.html │ ├── dir_ba769f4416b2c074a28c6130af43e345.html │ ├── dir_ba769f4416b2c074a28c6130af43e345.js │ ├── dir_c0ea682cca75c87761dacf1668992820.html │ ├── dir_c0ea682cca75c87761dacf1668992820.js │ ├── dir_c7fa37d54586c2d4e1bdb0bf9742bd86.html │ ├── dir_c7fa37d54586c2d4e1bdb0bf9742bd86.js │ ├── dir_ca7d207afbe6ec834644d82c9da0e27f.html │ ├── dir_ca7d207afbe6ec834644d82c9da0e27f.js │ ├── dir_cbab373848d17bb13c8f8154bda6a142.html │ ├── dir_cde5b50139b2efdd71913c8f6e2f5b92.html │ ├── dir_d0af3520e52159625e5c54b0a0666246.html │ ├── dir_d0af3520e52159625e5c54b0a0666246.js │ ├── dir_d28a4824dc47e487b107a5db32ef43c4.html │ ├── dir_dbdb02b1c6a463d71690e5d1f101f3be.html │ ├── dir_dbdb02b1c6a463d71690e5d1f101f3be.js │ ├── dir_e0a5a45ea46034a62177509fd4cc477b.html │ ├── dir_e55adf3e55c0cf2ab15afcf2abaa2799.html │ ├── dir_e8b3846043ed55e70c4740c96c71631d.html │ ├── dir_ec82c3f1a2edb4d01443ada27de98406.html │ ├── dir_ec82c3f1a2edb4d01443ada27de98406.js │ ├── dir_f5a6105ca7ea82175c57b6cc08e28f9f.html │ ├── dir_f82b846bb6a413b95a3fa0edffb6464f.html │ ├── doxygen.css │ ├── doxygen_crawl.html │ ├── dynsections.js │ ├── files.html │ ├── files_dup.js │ ├── functions.html │ ├── functions_b.html │ ├── functions_c.html │ ├── functions_d.html │ ├── functions_dup.js │ ├── functions_e.html │ ├── functions_evnt.html │ ├── functions_f.html │ ├── functions_func.html │ ├── functions_func.js │ ├── functions_func_b.html │ ├── functions_func_c.html │ ├── functions_func_d.html │ ├── functions_func_e.html │ ├── functions_func_f.html │ ├── functions_func_g.html │ ├── functions_func_h.html │ ├── functions_func_i.html │ ├── functions_func_j.html │ ├── functions_func_k.html │ ├── functions_func_l.html │ ├── functions_func_o.html │ ├── functions_func_p.html │ ├── functions_func_r.html │ ├── functions_func_s.html │ ├── functions_func_t.html │ ├── functions_func_u.html │ ├── functions_func_v.html │ ├── functions_func_w.html │ ├── functions_g.html │ ├── functions_h.html │ ├── functions_i.html │ ├── functions_j.html │ ├── functions_k.html │ ├── functions_l.html │ ├── functions_m.html │ ├── functions_n.html │ ├── functions_o.html │ ├── functions_p.html │ ├── functions_prop.html │ ├── functions_prop.js │ ├── functions_prop_b.html │ ├── functions_prop_c.html │ ├── functions_prop_d.html │ ├── functions_prop_e.html │ ├── functions_prop_f.html │ ├── functions_prop_g.html │ ├── functions_prop_h.html │ ├── functions_prop_i.html │ ├── functions_prop_j.html │ ├── functions_prop_k.html │ ├── functions_prop_l.html │ ├── functions_prop_m.html │ ├── functions_prop_n.html │ ├── functions_prop_o.html │ ├── functions_prop_p.html │ ├── functions_prop_r.html │ ├── functions_prop_s.html │ ├── functions_prop_t.html │ ├── functions_prop_u.html │ ├── functions_prop_v.html │ ├── functions_prop_w.html │ ├── functions_r.html │ ├── functions_s.html │ ├── functions_t.html │ ├── functions_u.html │ ├── functions_v.html │ ├── functions_vars.html │ ├── functions_w.html │ ├── hierarchy.html │ ├── hierarchy.js │ ├── index.html │ ├── interface_nakama_1_1_console_1_1_i_api_account_device-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_account_device.html │ ├── interface_nakama_1_1_console_1_1_i_api_account_device.js │ ├── interface_nakama_1_1_console_1_1_i_api_channel_message-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_channel_message.html │ ├── interface_nakama_1_1_console_1_1_i_api_channel_message.js │ ├── interface_nakama_1_1_console_1_1_i_api_friend-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_friend.html │ ├── interface_nakama_1_1_console_1_1_i_api_friend.js │ ├── interface_nakama_1_1_console_1_1_i_api_friend_list-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_friend_list.html │ ├── interface_nakama_1_1_console_1_1_i_api_friend_list.js │ ├── interface_nakama_1_1_console_1_1_i_api_group-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_group.html │ ├── interface_nakama_1_1_console_1_1_i_api_group.js │ ├── interface_nakama_1_1_console_1_1_i_api_leaderboard_record-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_leaderboard_record.html │ ├── interface_nakama_1_1_console_1_1_i_api_leaderboard_record.js │ ├── interface_nakama_1_1_console_1_1_i_api_notification-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_notification.html │ ├── interface_nakama_1_1_console_1_1_i_api_notification.js │ ├── interface_nakama_1_1_console_1_1_i_api_storage_object-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_storage_object.html │ ├── interface_nakama_1_1_console_1_1_i_api_storage_object.js │ ├── interface_nakama_1_1_console_1_1_i_api_storage_object_ack-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_storage_object_ack.html │ ├── interface_nakama_1_1_console_1_1_i_api_storage_object_ack.js │ ├── interface_nakama_1_1_console_1_1_i_api_user-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_user.html │ ├── interface_nakama_1_1_console_1_1_i_api_user.js │ ├── interface_nakama_1_1_console_1_1_i_api_user_group_list-members.html │ ├── interface_nakama_1_1_console_1_1_i_api_user_group_list.html │ ├── interface_nakama_1_1_console_1_1_i_api_user_group_list.js │ ├── interface_nakama_1_1_console_1_1_i_config_warning-members.html │ ├── interface_nakama_1_1_console_1_1_i_config_warning.html │ ├── interface_nakama_1_1_console_1_1_i_config_warning.js │ ├── interface_nakama_1_1_console_1_1_i_console_account_export-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_account_export.html │ ├── interface_nakama_1_1_console_1_1_i_console_account_export.js │ ├── interface_nakama_1_1_console_1_1_i_console_authenticate_request-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_authenticate_request.html │ ├── interface_nakama_1_1_console_1_1_i_console_authenticate_request.js │ ├── interface_nakama_1_1_console_1_1_i_console_config-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_config.html │ ├── interface_nakama_1_1_console_1_1_i_console_config.js │ ├── interface_nakama_1_1_console_1_1_i_console_console_session-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_console_session.html │ ├── interface_nakama_1_1_console_1_1_i_console_console_session.js │ ├── interface_nakama_1_1_console_1_1_i_console_status_list-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_status_list.html │ ├── interface_nakama_1_1_console_1_1_i_console_status_list.js │ ├── interface_nakama_1_1_console_1_1_i_console_storage_list-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_storage_list.html │ ├── interface_nakama_1_1_console_1_1_i_console_storage_list.js │ ├── interface_nakama_1_1_console_1_1_i_console_unlink_device_request-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_unlink_device_request.html │ ├── interface_nakama_1_1_console_1_1_i_console_unlink_device_request.js │ ├── interface_nakama_1_1_console_1_1_i_console_user_list-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_user_list.html │ ├── interface_nakama_1_1_console_1_1_i_console_user_list.js │ ├── interface_nakama_1_1_console_1_1_i_console_wallet_ledger-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_wallet_ledger.html │ ├── interface_nakama_1_1_console_1_1_i_console_wallet_ledger.js │ ├── interface_nakama_1_1_console_1_1_i_console_wallet_ledger_list-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_wallet_ledger_list.html │ ├── interface_nakama_1_1_console_1_1_i_console_wallet_ledger_list.js │ ├── interface_nakama_1_1_console_1_1_i_console_write_storage_object_request-members.html │ ├── interface_nakama_1_1_console_1_1_i_console_write_storage_object_request.html │ ├── interface_nakama_1_1_console_1_1_i_console_write_storage_object_request.js │ ├── interface_nakama_1_1_console_1_1_i_nakamaapi_account-members.html │ ├── interface_nakama_1_1_console_1_1_i_nakamaapi_account.html │ ├── interface_nakama_1_1_console_1_1_i_nakamaapi_account.js │ ├── interface_nakama_1_1_console_1_1_i_nakamaconsole_account-members.html │ ├── interface_nakama_1_1_console_1_1_i_nakamaconsole_account.html │ ├── interface_nakama_1_1_console_1_1_i_nakamaconsole_account.js │ ├── interface_nakama_1_1_console_1_1_i_nakamaconsole_update_account_request-members.html │ ├── interface_nakama_1_1_console_1_1_i_nakamaconsole_update_account_request.html │ ├── interface_nakama_1_1_console_1_1_i_nakamaconsole_update_account_request.js │ ├── interface_nakama_1_1_console_1_1_i_protobuf_any-members.html │ ├── interface_nakama_1_1_console_1_1_i_protobuf_any.html │ ├── interface_nakama_1_1_console_1_1_i_protobuf_any.js │ ├── interface_nakama_1_1_console_1_1_i_runtime_error-members.html │ ├── interface_nakama_1_1_console_1_1_i_runtime_error.html │ ├── interface_nakama_1_1_console_1_1_i_runtime_error.js │ ├── interface_nakama_1_1_console_1_1_i_status_list_status-members.html │ ├── interface_nakama_1_1_console_1_1_i_status_list_status.html │ ├── interface_nakama_1_1_console_1_1_i_status_list_status.js │ ├── interface_nakama_1_1_console_1_1_i_user_group_list_user_group-members.html │ ├── interface_nakama_1_1_console_1_1_i_user_group_list_user_group.html │ ├── interface_nakama_1_1_console_1_1_i_user_group_list_user_group.js │ ├── interface_nakama_1_1_i_api_account-members.html │ ├── interface_nakama_1_1_i_api_account.html │ ├── interface_nakama_1_1_i_api_account.js │ ├── interface_nakama_1_1_i_api_account_apple-members.html │ ├── interface_nakama_1_1_i_api_account_apple.html │ ├── interface_nakama_1_1_i_api_account_apple.js │ ├── interface_nakama_1_1_i_api_account_custom-members.html │ ├── interface_nakama_1_1_i_api_account_custom.html │ ├── interface_nakama_1_1_i_api_account_custom.js │ ├── interface_nakama_1_1_i_api_account_device-members.html │ ├── interface_nakama_1_1_i_api_account_device.html │ ├── interface_nakama_1_1_i_api_account_device.js │ ├── interface_nakama_1_1_i_api_account_email-members.html │ ├── interface_nakama_1_1_i_api_account_email.html │ ├── interface_nakama_1_1_i_api_account_email.js │ ├── interface_nakama_1_1_i_api_account_facebook-members.html │ ├── interface_nakama_1_1_i_api_account_facebook.html │ ├── interface_nakama_1_1_i_api_account_facebook.js │ ├── interface_nakama_1_1_i_api_account_facebook_instant_game-members.html │ ├── interface_nakama_1_1_i_api_account_facebook_instant_game.html │ ├── interface_nakama_1_1_i_api_account_facebook_instant_game.js │ ├── interface_nakama_1_1_i_api_account_game_center-members.html │ ├── interface_nakama_1_1_i_api_account_game_center.html │ ├── interface_nakama_1_1_i_api_account_game_center.js │ ├── interface_nakama_1_1_i_api_account_google-members.html │ ├── interface_nakama_1_1_i_api_account_google.html │ ├── interface_nakama_1_1_i_api_account_google.js │ ├── interface_nakama_1_1_i_api_account_steam-members.html │ ├── interface_nakama_1_1_i_api_account_steam.html │ ├── interface_nakama_1_1_i_api_account_steam.js │ ├── interface_nakama_1_1_i_api_channel_message-members.html │ ├── interface_nakama_1_1_i_api_channel_message.html │ ├── interface_nakama_1_1_i_api_channel_message.js │ ├── interface_nakama_1_1_i_api_channel_message_list-members.html │ ├── interface_nakama_1_1_i_api_channel_message_list.html │ ├── interface_nakama_1_1_i_api_channel_message_list.js │ ├── interface_nakama_1_1_i_api_create_group_request-members.html │ ├── interface_nakama_1_1_i_api_create_group_request.html │ ├── interface_nakama_1_1_i_api_create_group_request.js │ ├── interface_nakama_1_1_i_api_delete_storage_object_id-members.html │ ├── interface_nakama_1_1_i_api_delete_storage_object_id.html │ ├── interface_nakama_1_1_i_api_delete_storage_object_id.js │ ├── interface_nakama_1_1_i_api_delete_storage_objects_request-members.html │ ├── interface_nakama_1_1_i_api_delete_storage_objects_request.html │ ├── interface_nakama_1_1_i_api_delete_storage_objects_request.js │ ├── interface_nakama_1_1_i_api_event-members.html │ ├── interface_nakama_1_1_i_api_event.html │ ├── interface_nakama_1_1_i_api_event.js │ ├── interface_nakama_1_1_i_api_friend-members.html │ ├── interface_nakama_1_1_i_api_friend.html │ ├── interface_nakama_1_1_i_api_friend.js │ ├── interface_nakama_1_1_i_api_friend_list-members.html │ ├── interface_nakama_1_1_i_api_friend_list.html │ ├── interface_nakama_1_1_i_api_friend_list.js │ ├── interface_nakama_1_1_i_api_friends_of_friends_list-members.html │ ├── interface_nakama_1_1_i_api_friends_of_friends_list.html │ ├── interface_nakama_1_1_i_api_friends_of_friends_list.js │ ├── interface_nakama_1_1_i_api_group-members.html │ ├── interface_nakama_1_1_i_api_group.html │ ├── interface_nakama_1_1_i_api_group.js │ ├── interface_nakama_1_1_i_api_group_list-members.html │ ├── interface_nakama_1_1_i_api_group_list.html │ ├── interface_nakama_1_1_i_api_group_list.js │ ├── interface_nakama_1_1_i_api_group_user_list-members.html │ ├── interface_nakama_1_1_i_api_group_user_list.html │ ├── interface_nakama_1_1_i_api_group_user_list.js │ ├── interface_nakama_1_1_i_api_leaderboard_record-members.html │ ├── interface_nakama_1_1_i_api_leaderboard_record.html │ ├── interface_nakama_1_1_i_api_leaderboard_record.js │ ├── interface_nakama_1_1_i_api_leaderboard_record_list-members.html │ ├── interface_nakama_1_1_i_api_leaderboard_record_list.html │ ├── interface_nakama_1_1_i_api_leaderboard_record_list.js │ ├── interface_nakama_1_1_i_api_link_steam_request-members.html │ ├── interface_nakama_1_1_i_api_link_steam_request.html │ ├── interface_nakama_1_1_i_api_link_steam_request.js │ ├── interface_nakama_1_1_i_api_list_subscriptions_request-members.html │ ├── interface_nakama_1_1_i_api_list_subscriptions_request.html │ ├── interface_nakama_1_1_i_api_list_subscriptions_request.js │ ├── interface_nakama_1_1_i_api_match-members.html │ ├── interface_nakama_1_1_i_api_match.html │ ├── interface_nakama_1_1_i_api_match.js │ ├── interface_nakama_1_1_i_api_match_list-members.html │ ├── interface_nakama_1_1_i_api_match_list.html │ ├── interface_nakama_1_1_i_api_match_list.js │ ├── interface_nakama_1_1_i_api_matchmaker_completion_stats-members.html │ ├── interface_nakama_1_1_i_api_matchmaker_completion_stats.html │ ├── interface_nakama_1_1_i_api_matchmaker_completion_stats.js │ ├── interface_nakama_1_1_i_api_matchmaker_stats-members.html │ ├── interface_nakama_1_1_i_api_matchmaker_stats.html │ ├── interface_nakama_1_1_i_api_matchmaker_stats.js │ ├── interface_nakama_1_1_i_api_notification-members.html │ ├── interface_nakama_1_1_i_api_notification.html │ ├── interface_nakama_1_1_i_api_notification.js │ ├── interface_nakama_1_1_i_api_notification_list-members.html │ ├── interface_nakama_1_1_i_api_notification_list.html │ ├── interface_nakama_1_1_i_api_notification_list.js │ ├── interface_nakama_1_1_i_api_party-members.html │ ├── interface_nakama_1_1_i_api_party.html │ ├── interface_nakama_1_1_i_api_party.js │ ├── interface_nakama_1_1_i_api_party_list-members.html │ ├── interface_nakama_1_1_i_api_party_list.html │ ├── interface_nakama_1_1_i_api_party_list.js │ ├── interface_nakama_1_1_i_api_read_storage_object_id-members.html │ ├── interface_nakama_1_1_i_api_read_storage_object_id.html │ ├── interface_nakama_1_1_i_api_read_storage_object_id.js │ ├── interface_nakama_1_1_i_api_read_storage_objects_request-members.html │ ├── interface_nakama_1_1_i_api_read_storage_objects_request.html │ ├── interface_nakama_1_1_i_api_read_storage_objects_request.js │ ├── interface_nakama_1_1_i_api_rpc-members.html │ ├── interface_nakama_1_1_i_api_rpc.html │ ├── interface_nakama_1_1_i_api_rpc.js │ ├── interface_nakama_1_1_i_api_session-members.html │ ├── interface_nakama_1_1_i_api_session.html │ ├── interface_nakama_1_1_i_api_session.js │ ├── interface_nakama_1_1_i_api_session_logout_request-members.html │ ├── interface_nakama_1_1_i_api_session_logout_request.html │ ├── interface_nakama_1_1_i_api_session_logout_request.js │ ├── interface_nakama_1_1_i_api_session_refresh_request-members.html │ ├── interface_nakama_1_1_i_api_session_refresh_request.html │ ├── interface_nakama_1_1_i_api_session_refresh_request.js │ ├── interface_nakama_1_1_i_api_storage_object-members.html │ ├── interface_nakama_1_1_i_api_storage_object.html │ ├── interface_nakama_1_1_i_api_storage_object.js │ ├── interface_nakama_1_1_i_api_storage_object_ack-members.html │ ├── interface_nakama_1_1_i_api_storage_object_ack.html │ ├── interface_nakama_1_1_i_api_storage_object_ack.js │ ├── interface_nakama_1_1_i_api_storage_object_acks-members.html │ ├── interface_nakama_1_1_i_api_storage_object_acks.html │ ├── interface_nakama_1_1_i_api_storage_object_acks.js │ ├── interface_nakama_1_1_i_api_storage_object_list-members.html │ ├── interface_nakama_1_1_i_api_storage_object_list.html │ ├── interface_nakama_1_1_i_api_storage_object_list.js │ ├── interface_nakama_1_1_i_api_storage_objects-members.html │ ├── interface_nakama_1_1_i_api_storage_objects.html │ ├── interface_nakama_1_1_i_api_storage_objects.js │ ├── interface_nakama_1_1_i_api_subscription_list-members.html │ ├── interface_nakama_1_1_i_api_subscription_list.html │ ├── interface_nakama_1_1_i_api_subscription_list.js │ ├── interface_nakama_1_1_i_api_tournament-members.html │ ├── interface_nakama_1_1_i_api_tournament.html │ ├── interface_nakama_1_1_i_api_tournament.js │ ├── interface_nakama_1_1_i_api_tournament_list-members.html │ ├── interface_nakama_1_1_i_api_tournament_list.html │ ├── interface_nakama_1_1_i_api_tournament_list.js │ ├── interface_nakama_1_1_i_api_tournament_record_list-members.html │ ├── interface_nakama_1_1_i_api_tournament_record_list.html │ ├── interface_nakama_1_1_i_api_tournament_record_list.js │ ├── interface_nakama_1_1_i_api_update_account_request-members.html │ ├── interface_nakama_1_1_i_api_update_account_request.html │ ├── interface_nakama_1_1_i_api_update_account_request.js │ ├── interface_nakama_1_1_i_api_update_group_request-members.html │ ├── interface_nakama_1_1_i_api_update_group_request.html │ ├── interface_nakama_1_1_i_api_update_group_request.js │ ├── interface_nakama_1_1_i_api_user-members.html │ ├── interface_nakama_1_1_i_api_user.html │ ├── interface_nakama_1_1_i_api_user.js │ ├── interface_nakama_1_1_i_api_user_group_list-members.html │ ├── interface_nakama_1_1_i_api_user_group_list.html │ ├── interface_nakama_1_1_i_api_user_group_list.js │ ├── interface_nakama_1_1_i_api_users-members.html │ ├── interface_nakama_1_1_i_api_users.html │ ├── interface_nakama_1_1_i_api_users.js │ ├── interface_nakama_1_1_i_api_validate_purchase_apple_request-members.html │ ├── interface_nakama_1_1_i_api_validate_purchase_apple_request.html │ ├── interface_nakama_1_1_i_api_validate_purchase_apple_request.js │ ├── interface_nakama_1_1_i_api_validate_purchase_facebook_instant_request-members.html │ ├── interface_nakama_1_1_i_api_validate_purchase_facebook_instant_request.html │ ├── interface_nakama_1_1_i_api_validate_purchase_facebook_instant_request.js │ ├── interface_nakama_1_1_i_api_validate_purchase_google_request-members.html │ ├── interface_nakama_1_1_i_api_validate_purchase_google_request.html │ ├── interface_nakama_1_1_i_api_validate_purchase_google_request.js │ ├── interface_nakama_1_1_i_api_validate_purchase_huawei_request-members.html │ ├── interface_nakama_1_1_i_api_validate_purchase_huawei_request.html │ ├── interface_nakama_1_1_i_api_validate_purchase_huawei_request.js │ ├── interface_nakama_1_1_i_api_validate_purchase_response-members.html │ ├── interface_nakama_1_1_i_api_validate_purchase_response.html │ ├── interface_nakama_1_1_i_api_validate_purchase_response.js │ ├── interface_nakama_1_1_i_api_validate_subscription_apple_request-members.html │ ├── interface_nakama_1_1_i_api_validate_subscription_apple_request.html │ ├── interface_nakama_1_1_i_api_validate_subscription_apple_request.js │ ├── interface_nakama_1_1_i_api_validate_subscription_google_request-members.html │ ├── interface_nakama_1_1_i_api_validate_subscription_google_request.html │ ├── interface_nakama_1_1_i_api_validate_subscription_google_request.js │ ├── interface_nakama_1_1_i_api_validate_subscription_response-members.html │ ├── interface_nakama_1_1_i_api_validate_subscription_response.html │ ├── interface_nakama_1_1_i_api_validate_subscription_response.js │ ├── interface_nakama_1_1_i_api_validated_purchase-members.html │ ├── interface_nakama_1_1_i_api_validated_purchase.html │ ├── interface_nakama_1_1_i_api_validated_purchase.js │ ├── interface_nakama_1_1_i_api_validated_subscription-members.html │ ├── interface_nakama_1_1_i_api_validated_subscription.html │ ├── interface_nakama_1_1_i_api_validated_subscription.js │ ├── interface_nakama_1_1_i_api_write_storage_object-members.html │ ├── interface_nakama_1_1_i_api_write_storage_object.html │ ├── interface_nakama_1_1_i_api_write_storage_object.js │ ├── interface_nakama_1_1_i_api_write_storage_objects_request-members.html │ ├── interface_nakama_1_1_i_api_write_storage_objects_request.html │ ├── interface_nakama_1_1_i_api_write_storage_objects_request.js │ ├── interface_nakama_1_1_i_channel-members.html │ ├── interface_nakama_1_1_i_channel.html │ ├── interface_nakama_1_1_i_channel.js │ ├── interface_nakama_1_1_i_channel_message_ack-members.html │ ├── interface_nakama_1_1_i_channel_message_ack.html │ ├── interface_nakama_1_1_i_channel_message_ack.js │ ├── interface_nakama_1_1_i_channel_presence_event-members.html │ ├── interface_nakama_1_1_i_channel_presence_event.html │ ├── interface_nakama_1_1_i_channel_presence_event.js │ ├── interface_nakama_1_1_i_client-members.html │ ├── interface_nakama_1_1_i_client.html │ ├── interface_nakama_1_1_i_client.js │ ├── interface_nakama_1_1_i_friends_of_friends_list_friend_of_friend-members.html │ ├── interface_nakama_1_1_i_friends_of_friends_list_friend_of_friend.html │ ├── interface_nakama_1_1_i_friends_of_friends_list_friend_of_friend.js │ ├── interface_nakama_1_1_i_group_user_list_group_user-members.html │ ├── interface_nakama_1_1_i_group_user_list_group_user.html │ ├── interface_nakama_1_1_i_group_user_list_group_user.js │ ├── interface_nakama_1_1_i_http_adapter-members.html │ ├── interface_nakama_1_1_i_http_adapter.html │ ├── interface_nakama_1_1_i_http_adapter.js │ ├── interface_nakama_1_1_i_logger-members.html │ ├── interface_nakama_1_1_i_logger.html │ ├── interface_nakama_1_1_i_logger.js │ ├── interface_nakama_1_1_i_match-members.html │ ├── interface_nakama_1_1_i_match.html │ ├── interface_nakama_1_1_i_match.js │ ├── interface_nakama_1_1_i_match_presence_event-members.html │ ├── interface_nakama_1_1_i_match_presence_event.html │ ├── interface_nakama_1_1_i_match_presence_event.js │ ├── interface_nakama_1_1_i_match_state-members.html │ ├── interface_nakama_1_1_i_match_state.html │ ├── interface_nakama_1_1_i_match_state.js │ ├── interface_nakama_1_1_i_matchmaker_matched-members.html │ ├── interface_nakama_1_1_i_matchmaker_matched.html │ ├── interface_nakama_1_1_i_matchmaker_matched.js │ ├── interface_nakama_1_1_i_matchmaker_ticket-members.html │ ├── interface_nakama_1_1_i_matchmaker_ticket.html │ ├── interface_nakama_1_1_i_matchmaker_ticket.js │ ├── interface_nakama_1_1_i_matchmaker_user-members.html │ ├── interface_nakama_1_1_i_matchmaker_user.html │ ├── interface_nakama_1_1_i_matchmaker_user.js │ ├── interface_nakama_1_1_i_party-members.html │ ├── interface_nakama_1_1_i_party.html │ ├── interface_nakama_1_1_i_party.js │ ├── interface_nakama_1_1_i_party_close-members.html │ ├── interface_nakama_1_1_i_party_close.html │ ├── interface_nakama_1_1_i_party_close.js │ ├── interface_nakama_1_1_i_party_data-members.html │ ├── interface_nakama_1_1_i_party_data.html │ ├── interface_nakama_1_1_i_party_data.js │ ├── interface_nakama_1_1_i_party_join_request-members.html │ ├── interface_nakama_1_1_i_party_join_request.html │ ├── interface_nakama_1_1_i_party_join_request.js │ ├── interface_nakama_1_1_i_party_leader-members.html │ ├── interface_nakama_1_1_i_party_leader.html │ ├── interface_nakama_1_1_i_party_leader.js │ ├── interface_nakama_1_1_i_party_matchmaker_ticket-members.html │ ├── interface_nakama_1_1_i_party_matchmaker_ticket.html │ ├── interface_nakama_1_1_i_party_matchmaker_ticket.js │ ├── interface_nakama_1_1_i_party_presence_event-members.html │ ├── interface_nakama_1_1_i_party_presence_event.html │ ├── interface_nakama_1_1_i_party_presence_event.js │ ├── interface_nakama_1_1_i_party_update-members.html │ ├── interface_nakama_1_1_i_party_update.html │ ├── interface_nakama_1_1_i_party_update.js │ ├── interface_nakama_1_1_i_protobuf_any-members.html │ ├── interface_nakama_1_1_i_protobuf_any.html │ ├── interface_nakama_1_1_i_protobuf_any.js │ ├── interface_nakama_1_1_i_rpc_status-members.html │ ├── interface_nakama_1_1_i_rpc_status.html │ ├── interface_nakama_1_1_i_rpc_status.js │ ├── interface_nakama_1_1_i_session-members.html │ ├── interface_nakama_1_1_i_session.html │ ├── interface_nakama_1_1_i_session.js │ ├── interface_nakama_1_1_i_socket-members.html │ ├── interface_nakama_1_1_i_socket.html │ ├── interface_nakama_1_1_i_socket.js │ ├── interface_nakama_1_1_i_socket_adapter-members.html │ ├── interface_nakama_1_1_i_socket_adapter.html │ ├── interface_nakama_1_1_i_socket_adapter.js │ ├── interface_nakama_1_1_i_status-members.html │ ├── interface_nakama_1_1_i_status.html │ ├── interface_nakama_1_1_i_status.js │ ├── interface_nakama_1_1_i_status_presence_event-members.html │ ├── interface_nakama_1_1_i_status_presence_event.html │ ├── interface_nakama_1_1_i_status_presence_event.js │ ├── interface_nakama_1_1_i_stream-members.html │ ├── interface_nakama_1_1_i_stream.html │ ├── interface_nakama_1_1_i_stream.js │ ├── interface_nakama_1_1_i_stream_presence_event-members.html │ ├── interface_nakama_1_1_i_stream_presence_event.html │ ├── interface_nakama_1_1_i_stream_presence_event.js │ ├── interface_nakama_1_1_i_stream_state-members.html │ ├── interface_nakama_1_1_i_stream_state.html │ ├── interface_nakama_1_1_i_stream_state.js │ ├── interface_nakama_1_1_i_user_group_list_user_group-members.html │ ├── interface_nakama_1_1_i_user_group_list_user_group.html │ ├── interface_nakama_1_1_i_user_group_list_user_group.js │ ├── interface_nakama_1_1_i_user_presence-members.html │ ├── interface_nakama_1_1_i_user_presence.html │ ├── interface_nakama_1_1_i_user_presence.js │ ├── interface_nakama_1_1_i_write_leaderboard_record_request_leaderboard_record_write-members.html │ ├── interface_nakama_1_1_i_write_leaderboard_record_request_leaderboard_record_write.html │ ├── interface_nakama_1_1_i_write_leaderboard_record_request_leaderboard_record_write.js │ ├── interface_nakama_1_1_i_write_tournament_record_request_tournament_record_write-members.html │ ├── interface_nakama_1_1_i_write_tournament_record_request_tournament_record_write.html │ ├── interface_nakama_1_1_i_write_tournament_record_request_tournament_record_write.js │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_buffer_pool-members.html │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_buffer_pool.html │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_buffer_pool.js │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_ping_pong_manager-members.html │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_ping_pong_manager.html │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_ping_pong_manager.js │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_client_factory-members.html │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_client_factory.html │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_client_factory.js │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_server_factory-members.html │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_server_factory.html │ ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_server_factory.js │ ├── interface_nakama_1_1_tests_1_1_i_nested_test_object-members.html │ ├── interface_nakama_1_1_tests_1_1_i_nested_test_object.html │ ├── interface_nakama_1_1_tests_1_1_i_test_object-members.html │ ├── interface_nakama_1_1_tests_1_1_i_test_object.html │ ├── interface_satori_1_1_i_api_authenticate_logout_request-members.html │ ├── interface_satori_1_1_i_api_authenticate_logout_request.html │ ├── interface_satori_1_1_i_api_authenticate_logout_request.js │ ├── interface_satori_1_1_i_api_authenticate_refresh_request-members.html │ ├── interface_satori_1_1_i_api_authenticate_refresh_request.html │ ├── interface_satori_1_1_i_api_authenticate_refresh_request.js │ ├── interface_satori_1_1_i_api_authenticate_request-members.html │ ├── interface_satori_1_1_i_api_authenticate_request.html │ ├── interface_satori_1_1_i_api_authenticate_request.js │ ├── interface_satori_1_1_i_api_event-members.html │ ├── interface_satori_1_1_i_api_event.html │ ├── interface_satori_1_1_i_api_event.js │ ├── interface_satori_1_1_i_api_event_request-members.html │ ├── interface_satori_1_1_i_api_event_request.html │ ├── interface_satori_1_1_i_api_event_request.js │ ├── interface_satori_1_1_i_api_experiment-members.html │ ├── interface_satori_1_1_i_api_experiment.html │ ├── interface_satori_1_1_i_api_experiment.js │ ├── interface_satori_1_1_i_api_experiment_list-members.html │ ├── interface_satori_1_1_i_api_experiment_list.html │ ├── interface_satori_1_1_i_api_experiment_list.js │ ├── interface_satori_1_1_i_api_flag-members.html │ ├── interface_satori_1_1_i_api_flag.html │ ├── interface_satori_1_1_i_api_flag.js │ ├── interface_satori_1_1_i_api_flag_list-members.html │ ├── interface_satori_1_1_i_api_flag_list.html │ ├── interface_satori_1_1_i_api_flag_list.js │ ├── interface_satori_1_1_i_api_flag_override-members.html │ ├── interface_satori_1_1_i_api_flag_override.html │ ├── interface_satori_1_1_i_api_flag_override.js │ ├── interface_satori_1_1_i_api_flag_override_list-members.html │ ├── interface_satori_1_1_i_api_flag_override_list.html │ ├── interface_satori_1_1_i_api_flag_override_list.js │ ├── interface_satori_1_1_i_api_flag_override_value-members.html │ ├── interface_satori_1_1_i_api_flag_override_value.html │ ├── interface_satori_1_1_i_api_flag_override_value.js │ ├── interface_satori_1_1_i_api_get_message_list_response-members.html │ ├── interface_satori_1_1_i_api_get_message_list_response.html │ ├── interface_satori_1_1_i_api_get_message_list_response.js │ ├── interface_satori_1_1_i_api_identify_request-members.html │ ├── interface_satori_1_1_i_api_identify_request.html │ ├── interface_satori_1_1_i_api_identify_request.js │ ├── interface_satori_1_1_i_api_live_event-members.html │ ├── interface_satori_1_1_i_api_live_event.html │ ├── interface_satori_1_1_i_api_live_event.js │ ├── interface_satori_1_1_i_api_live_event_list-members.html │ ├── interface_satori_1_1_i_api_live_event_list.html │ ├── interface_satori_1_1_i_api_live_event_list.js │ ├── interface_satori_1_1_i_api_message-members.html │ ├── interface_satori_1_1_i_api_message.html │ ├── interface_satori_1_1_i_api_message.js │ ├── interface_satori_1_1_i_api_properties-members.html │ ├── interface_satori_1_1_i_api_properties.html │ ├── interface_satori_1_1_i_api_properties.js │ ├── interface_satori_1_1_i_api_session-members.html │ ├── interface_satori_1_1_i_api_session.html │ ├── interface_satori_1_1_i_api_session.js │ ├── interface_satori_1_1_i_api_update_message_request-members.html │ ├── interface_satori_1_1_i_api_update_message_request.html │ ├── interface_satori_1_1_i_api_update_message_request.js │ ├── interface_satori_1_1_i_api_update_properties_request-members.html │ ├── interface_satori_1_1_i_api_update_properties_request.html │ ├── interface_satori_1_1_i_api_update_properties_request.js │ ├── interface_satori_1_1_i_client-members.html │ ├── interface_satori_1_1_i_client.html │ ├── interface_satori_1_1_i_client.js │ ├── interface_satori_1_1_i_flag_value_change_reason-members.html │ ├── interface_satori_1_1_i_flag_value_change_reason.html │ ├── interface_satori_1_1_i_flag_value_change_reason.js │ ├── interface_satori_1_1_i_http_adapter-members.html │ ├── interface_satori_1_1_i_http_adapter.html │ ├── interface_satori_1_1_i_http_adapter.js │ ├── interface_satori_1_1_i_logger-members.html │ ├── interface_satori_1_1_i_logger.html │ ├── interface_satori_1_1_i_logger.js │ ├── interface_satori_1_1_i_protobuf_any-members.html │ ├── interface_satori_1_1_i_protobuf_any.html │ ├── interface_satori_1_1_i_protobuf_any.js │ ├── interface_satori_1_1_i_rpc_status-members.html │ ├── interface_satori_1_1_i_rpc_status.html │ ├── interface_satori_1_1_i_rpc_status.js │ ├── interface_satori_1_1_i_session-members.html │ ├── interface_satori_1_1_i_session.html │ ├── interface_satori_1_1_i_session.js │ ├── jquery.js │ ├── md__2_users_2flavio_2_projects_2heroiclabs_2nakama-dotnet_2_c_h_a_n_g_e_l_o_g.html │ ├── md__2_users_2gp_2_documents_2_git_hub_2nakama-dotnet_2_c_h_a_n_g_e_l_o_g.html │ ├── md__2_users_2gp_2_git_hub_2nakama-dotnet_2_c_h_a_n_g_e_l_o_g.html │ ├── md__2_users_2joao_2_projects_2heroic_2nakama-dotnet_2_c_h_a_n_g_e_l_o_g.html │ ├── md__2_users_2novabyte_2_heroic-_labs_2_projects_2nakama-dotnet_2_c_h_a_n_g_e_l_o_g.html │ ├── md___users_sean__documents__git_hub_nakama_dotnet__c_h_a_n_g_e_l_o_g.html │ ├── md___users_tom_heroic_projects_nakama_dotnet__c_h_a_n_g_e_l_o_g.html │ ├── md__c_h_a_n_g_e_l_o_g.html │ ├── md__r_e_a_d_m_e.html │ ├── md__r_e_l_e_a_s_e_i_n_s_t.html │ ├── md__satori__tests__r_e_a_d_m_e.html │ ├── md_codegen__r_e_a_d_m_e.html │ ├── menu.js │ ├── menudata.js │ ├── namespace_nakama.html │ ├── namespace_nakama.js │ ├── namespace_nakama_1_1_console.html │ ├── namespace_nakama_1_1_console.js │ ├── namespace_nakama_1_1_ninja.html │ ├── namespace_nakama_1_1_ninja.js │ ├── namespace_nakama_1_1_ninja_1_1_web_sockets.html │ ├── namespace_nakama_1_1_ninja_1_1_web_sockets.js │ ├── namespace_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions.html │ ├── namespace_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions.js │ ├── namespace_nakama_1_1_ninja_1_1_web_sockets_1_1_internal.html │ ├── namespace_nakama_1_1_tests.html │ ├── namespace_nakama_1_1_tests.js │ ├── namespace_nakama_1_1_tests_1_1_api.html │ ├── namespace_nakama_1_1_tests_1_1_api.js │ ├── namespace_nakama_1_1_tests_1_1_socket.html │ ├── namespace_nakama_1_1_tests_1_1_socket.js │ ├── namespace_nakama_1_1_tiny_json.html │ ├── namespace_satori.html │ ├── namespace_satori.js │ ├── namespace_satori_1_1_tests.html │ ├── namespace_satori_1_1_tests.js │ ├── namespace_satori_1_1_tiny_json.html │ ├── namespacemembers.html │ ├── namespacemembers_enum.html │ ├── namespacemembers_func.html │ ├── namespaces.html │ ├── namespaces_dup.js │ ├── navtree.css │ ├── navtree.js │ ├── navtreedata.js │ ├── navtreeindex0.js │ ├── navtreeindex1.js │ ├── navtreeindex10.js │ ├── navtreeindex11.js │ ├── navtreeindex12.js │ ├── navtreeindex13.js │ ├── navtreeindex2.js │ ├── navtreeindex3.js │ ├── navtreeindex4.js │ ├── navtreeindex5.js │ ├── navtreeindex6.js │ ├── navtreeindex7.js │ ├── navtreeindex8.js │ ├── navtreeindex9.js │ ├── net46_2_nakama_8_assembly_info_8cs.html │ ├── net46_2_satori_8_assembly_info_8cs.html │ ├── netstandard2_80_2_nakama_8_assembly_info_8cs.html │ ├── netstandard2_80_2_satori_8_assembly_info_8cs.html │ ├── pages.html │ ├── resize.js │ ├── search/ │ │ ├── all_0.html │ │ ├── all_0.js │ │ ├── all_1.html │ │ ├── all_1.js │ │ ├── all_10.html │ │ ├── all_10.js │ │ ├── all_11.html │ │ ├── all_11.js │ │ ├── all_12.html │ │ ├── all_12.js │ │ ├── all_13.html │ │ ├── all_13.js │ │ ├── all_14.html │ │ ├── all_14.js │ │ ├── all_15.html │ │ ├── all_15.js │ │ ├── all_2.html │ │ ├── all_2.js │ │ ├── all_3.html │ │ ├── all_3.js │ │ ├── all_4.html │ │ ├── all_4.js │ │ ├── all_5.html │ │ ├── all_5.js │ │ ├── all_6.html │ │ ├── all_6.js │ │ ├── all_7.html │ │ ├── all_7.js │ │ ├── all_8.html │ │ ├── all_8.js │ │ ├── all_9.html │ │ ├── all_9.js │ │ ├── all_a.html │ │ ├── all_a.js │ │ ├── all_b.html │ │ ├── all_b.js │ │ ├── all_c.html │ │ ├── all_c.js │ │ ├── all_d.html │ │ ├── all_d.js │ │ ├── all_e.html │ │ ├── all_e.js │ │ ├── all_f.html │ │ ├── all_f.js │ │ ├── classes_0.html │ │ ├── classes_0.js │ │ ├── classes_1.html │ │ ├── classes_1.js │ │ ├── classes_2.html │ │ ├── classes_2.js │ │ ├── classes_3.html │ │ ├── classes_3.js │ │ ├── classes_4.html │ │ ├── classes_4.js │ │ ├── classes_5.html │ │ ├── classes_5.js │ │ ├── classes_6.html │ │ ├── classes_6.js │ │ ├── classes_7.html │ │ ├── classes_7.js │ │ ├── classes_8.html │ │ ├── classes_8.js │ │ ├── classes_9.html │ │ ├── classes_9.js │ │ ├── classes_a.js │ │ ├── classes_b.js │ │ ├── classes_c.js │ │ ├── enums_0.html │ │ ├── enums_0.js │ │ ├── enums_1.html │ │ ├── enums_1.js │ │ ├── enums_2.js │ │ ├── enumvalues_0.html │ │ ├── enumvalues_0.js │ │ ├── enumvalues_1.html │ │ ├── enumvalues_1.js │ │ ├── enumvalues_2.html │ │ ├── enumvalues_2.js │ │ ├── enumvalues_3.html │ │ ├── enumvalues_3.js │ │ ├── enumvalues_4.html │ │ ├── enumvalues_4.js │ │ ├── enumvalues_5.html │ │ ├── enumvalues_5.js │ │ ├── enumvalues_6.html │ │ ├── enumvalues_6.js │ │ ├── enumvalues_7.html │ │ ├── enumvalues_7.js │ │ ├── enumvalues_8.js │ │ ├── enumvalues_9.js │ │ ├── enumvalues_a.js │ │ ├── enumvalues_b.js │ │ ├── enumvalues_c.js │ │ ├── enumvalues_d.js │ │ ├── events_0.html │ │ ├── events_0.js │ │ ├── events_1.html │ │ ├── events_1.js │ │ ├── events_2.html │ │ ├── events_2.js │ │ ├── files_0.html │ │ ├── files_0.js │ │ ├── files_1.html │ │ ├── files_1.js │ │ ├── files_2.html │ │ ├── files_2.js │ │ ├── files_3.html │ │ ├── files_3.js │ │ ├── files_4.html │ │ ├── files_4.js │ │ ├── files_5.html │ │ ├── files_5.js │ │ ├── files_6.html │ │ ├── files_6.js │ │ ├── files_7.html │ │ ├── files_7.js │ │ ├── files_8.html │ │ ├── files_8.js │ │ ├── files_9.html │ │ ├── files_9.js │ │ ├── files_a.html │ │ ├── files_a.js │ │ ├── files_b.html │ │ ├── files_b.js │ │ ├── files_c.html │ │ ├── files_c.js │ │ ├── files_d.html │ │ ├── files_d.js │ │ ├── files_e.js │ │ ├── files_f.js │ │ ├── functions_0.html │ │ ├── functions_0.js │ │ ├── functions_1.html │ │ ├── functions_1.js │ │ ├── functions_10.html │ │ ├── functions_10.js │ │ ├── functions_11.html │ │ ├── functions_11.js │ │ ├── functions_12.html │ │ ├── functions_12.js │ │ ├── functions_13.html │ │ ├── functions_13.js │ │ ├── functions_2.html │ │ ├── functions_2.js │ │ ├── functions_3.html │ │ ├── functions_3.js │ │ ├── functions_4.html │ │ ├── functions_4.js │ │ ├── functions_5.html │ │ ├── functions_5.js │ │ ├── functions_6.html │ │ ├── functions_6.js │ │ ├── functions_7.html │ │ ├── functions_7.js │ │ ├── functions_8.html │ │ ├── functions_8.js │ │ ├── functions_9.html │ │ ├── functions_9.js │ │ ├── functions_a.html │ │ ├── functions_a.js │ │ ├── functions_b.html │ │ ├── functions_b.js │ │ ├── functions_c.html │ │ ├── functions_c.js │ │ ├── functions_d.html │ │ ├── functions_d.js │ │ ├── functions_e.html │ │ ├── functions_e.js │ │ ├── functions_f.html │ │ ├── functions_f.js │ │ ├── namespaces_0.html │ │ ├── namespaces_0.js │ │ ├── namespaces_1.js │ │ ├── nomatches.html │ │ ├── pages_0.html │ │ ├── pages_0.js │ │ ├── pages_1.html │ │ ├── pages_1.js │ │ ├── pages_2.js │ │ ├── properties_0.html │ │ ├── properties_0.js │ │ ├── properties_1.html │ │ ├── properties_1.js │ │ ├── properties_10.html │ │ ├── properties_10.js │ │ ├── properties_11.html │ │ ├── properties_11.js │ │ ├── properties_12.html │ │ ├── properties_12.js │ │ ├── properties_13.html │ │ ├── properties_13.js │ │ ├── properties_14.html │ │ ├── properties_14.js │ │ ├── properties_15.html │ │ ├── properties_15.js │ │ ├── properties_2.html │ │ ├── properties_2.js │ │ ├── properties_3.html │ │ ├── properties_3.js │ │ ├── properties_4.html │ │ ├── properties_4.js │ │ ├── properties_5.html │ │ ├── properties_5.js │ │ ├── properties_6.html │ │ ├── properties_6.js │ │ ├── properties_7.html │ │ ├── properties_7.js │ │ ├── properties_8.html │ │ ├── properties_8.js │ │ ├── properties_9.html │ │ ├── properties_9.js │ │ ├── properties_a.html │ │ ├── properties_a.js │ │ ├── properties_b.html │ │ ├── properties_b.js │ │ ├── properties_c.html │ │ ├── properties_c.js │ │ ├── properties_d.html │ │ ├── properties_d.js │ │ ├── properties_e.html │ │ ├── properties_e.js │ │ ├── properties_f.html │ │ ├── properties_f.js │ │ ├── search.css │ │ ├── search.js │ │ ├── searchdata.js │ │ ├── variables_0.html │ │ ├── variables_0.js │ │ ├── variables_1.html │ │ └── variables_1.js │ └── tabs.css ├── index.html └── latex/ ├── Makefile ├── annotated.tex ├── class_nakama_1_1_api_response_exception.eps ├── class_nakama_1_1_api_response_exception.tex ├── class_nakama_1_1_client.eps ├── class_nakama_1_1_client.tex ├── class_nakama_1_1_console_1_1_api_response_exception.eps ├── class_nakama_1_1_console_1_1_api_response_exception.tex ├── class_nakama_1_1_http_request_adapter.eps ├── class_nakama_1_1_http_request_adapter.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool_1_1_public_buffer_memory_stream.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_buffer_pool_1_1_public_buffer_memory_stream.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_entity_too_large_exception.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_entity_too_large_exception.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_invalid_http_response_code_exception.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_invalid_http_response_code_exception.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_sec_web_socket_key_missing_exception.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_sec_web_socket_key_missing_exception.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_server_listener_socket_exception.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_server_listener_socket_exception.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_buffer_overflow_exception.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_buffer_overflow_exception.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_handshake_failed_exception.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_handshake_failed_exception.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_version_not_supported_exception.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions_1_1_web_socket_version_not_supported_exception.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_http_helper.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_ping_pong_manager.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_ping_pong_manager.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_pong_event_args.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_pong_event_args.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_client_factory.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_client_factory.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_client_options.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_http_context.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_server_factory.eps ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_server_factory.tex ├── class_nakama_1_1_ninja_1_1_web_sockets_1_1_web_socket_server_options.tex ├── class_nakama_1_1_retry.tex ├── class_nakama_1_1_retry_configuration.tex ├── class_nakama_1_1_session.eps ├── class_nakama_1_1_session.tex ├── class_nakama_1_1_socket.eps ├── class_nakama_1_1_socket.tex ├── class_nakama_1_1_storage_object_id.eps ├── class_nakama_1_1_storage_object_id.tex ├── class_nakama_1_1_tests_1_1_api_1_1_authenticate_test.tex ├── class_nakama_1_1_tests_1_1_api_1_1_group_test.tex ├── class_nakama_1_1_tests_1_1_api_1_1_http_error_test.tex ├── class_nakama_1_1_tests_1_1_api_1_1_leaderboard_around_owner_test.eps ├── class_nakama_1_1_tests_1_1_api_1_1_leaderboard_around_owner_test.tex ├── class_nakama_1_1_tests_1_1_api_1_1_leaderboard_test.eps ├── class_nakama_1_1_tests_1_1_api_1_1_leaderboard_test.tex ├── class_nakama_1_1_tests_1_1_api_1_1_link_unlink_test.tex ├── class_nakama_1_1_tests_1_1_api_1_1_rpc_test.tex ├── class_nakama_1_1_tests_1_1_awaited_socket_task_test.eps ├── class_nakama_1_1_tests_1_1_awaited_socket_task_test.tex ├── class_nakama_1_1_tests_1_1_cancel_test.tex ├── class_nakama_1_1_tests_1_1_retry_test.tex ├── class_nakama_1_1_tests_1_1_session_test.tex ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_channel_test.eps ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_channel_test.tex ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_match_test.eps ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_match_test.tex ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_matchmaker_test.eps ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_matchmaker_test.tex ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_notification_test.eps ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_notification_test.tex ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_party_test.tex ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_rpc_test.eps ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_rpc_test.tex ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_test.tex ├── class_nakama_1_1_tests_1_1_socket_1_1_web_socket_user_status_test.tex ├── class_nakama_1_1_tests_1_1_stdout_logger.eps ├── class_nakama_1_1_tests_1_1_stdout_logger.tex ├── class_nakama_1_1_tests_1_1_tiny_json_parser_test.tex ├── class_nakama_1_1_tests_1_1_transient_exception_http_adapter.eps ├── class_nakama_1_1_tests_1_1_transient_exception_http_adapter.tex ├── class_nakama_1_1_web_socket_adapter.eps ├── class_nakama_1_1_web_socket_adapter.tex ├── class_nakama_1_1_web_socket_stdlib_adapter.eps ├── class_nakama_1_1_web_socket_stdlib_adapter.tex ├── class_nakama_1_1_write_storage_object.eps ├── class_nakama_1_1_write_storage_object.tex ├── class_satori_1_1_api_response_exception.eps ├── class_satori_1_1_api_response_exception.tex ├── class_satori_1_1_client.eps ├── class_satori_1_1_client.tex ├── class_satori_1_1_event.tex ├── class_satori_1_1_http_request_adapter.eps ├── class_satori_1_1_http_request_adapter.tex ├── class_satori_1_1_session.eps ├── class_satori_1_1_session.tex ├── class_satori_1_1_tests_1_1_client_test.tex ├── doxygen.sty ├── hierarchy.tex ├── index.tex ├── interface_nakama_1_1_console_1_1_i_api_account_device.tex ├── interface_nakama_1_1_console_1_1_i_api_channel_message.tex ├── interface_nakama_1_1_console_1_1_i_api_friend.tex ├── interface_nakama_1_1_console_1_1_i_api_friend_list.tex ├── interface_nakama_1_1_console_1_1_i_api_group.tex ├── interface_nakama_1_1_console_1_1_i_api_leaderboard_record.tex ├── interface_nakama_1_1_console_1_1_i_api_notification.tex ├── interface_nakama_1_1_console_1_1_i_api_storage_object.tex ├── interface_nakama_1_1_console_1_1_i_api_storage_object_ack.tex ├── interface_nakama_1_1_console_1_1_i_api_user.tex ├── interface_nakama_1_1_console_1_1_i_api_user_group_list.tex ├── interface_nakama_1_1_console_1_1_i_config_warning.tex ├── interface_nakama_1_1_console_1_1_i_console_account_export.tex ├── interface_nakama_1_1_console_1_1_i_console_authenticate_request.tex ├── interface_nakama_1_1_console_1_1_i_console_config.tex ├── interface_nakama_1_1_console_1_1_i_console_console_session.tex ├── interface_nakama_1_1_console_1_1_i_console_status_list.tex ├── interface_nakama_1_1_console_1_1_i_console_storage_list.tex ├── interface_nakama_1_1_console_1_1_i_console_unlink_device_request.tex ├── interface_nakama_1_1_console_1_1_i_console_user_list.tex ├── interface_nakama_1_1_console_1_1_i_console_wallet_ledger.tex ├── interface_nakama_1_1_console_1_1_i_console_wallet_ledger_list.tex ├── interface_nakama_1_1_console_1_1_i_console_write_storage_object_request.tex ├── interface_nakama_1_1_console_1_1_i_nakamaapi_account.tex ├── interface_nakama_1_1_console_1_1_i_nakamaconsole_account.tex ├── interface_nakama_1_1_console_1_1_i_nakamaconsole_update_account_request.tex ├── interface_nakama_1_1_console_1_1_i_protobuf_any.tex ├── interface_nakama_1_1_console_1_1_i_runtime_error.tex ├── interface_nakama_1_1_console_1_1_i_status_list_status.tex ├── interface_nakama_1_1_console_1_1_i_user_group_list_user_group.tex ├── interface_nakama_1_1_i_api_account.tex ├── interface_nakama_1_1_i_api_account_apple.tex ├── interface_nakama_1_1_i_api_account_custom.tex ├── interface_nakama_1_1_i_api_account_device.tex ├── interface_nakama_1_1_i_api_account_email.tex ├── interface_nakama_1_1_i_api_account_facebook.tex ├── interface_nakama_1_1_i_api_account_facebook_instant_game.tex ├── interface_nakama_1_1_i_api_account_game_center.tex ├── interface_nakama_1_1_i_api_account_google.tex ├── interface_nakama_1_1_i_api_account_steam.tex ├── interface_nakama_1_1_i_api_channel_message.tex ├── interface_nakama_1_1_i_api_channel_message_list.tex ├── interface_nakama_1_1_i_api_create_group_request.tex ├── interface_nakama_1_1_i_api_delete_storage_object_id.eps ├── interface_nakama_1_1_i_api_delete_storage_object_id.tex ├── interface_nakama_1_1_i_api_delete_storage_objects_request.tex ├── interface_nakama_1_1_i_api_event.tex ├── interface_nakama_1_1_i_api_friend.tex ├── interface_nakama_1_1_i_api_friend_list.tex ├── interface_nakama_1_1_i_api_group.tex ├── interface_nakama_1_1_i_api_group_list.tex ├── interface_nakama_1_1_i_api_group_user_list.tex ├── interface_nakama_1_1_i_api_leaderboard_record.tex ├── interface_nakama_1_1_i_api_leaderboard_record_list.tex ├── interface_nakama_1_1_i_api_link_steam_request.tex ├── interface_nakama_1_1_i_api_list_subscriptions_request.tex ├── interface_nakama_1_1_i_api_match.tex ├── interface_nakama_1_1_i_api_match_list.tex ├── interface_nakama_1_1_i_api_notification.tex ├── interface_nakama_1_1_i_api_notification_list.tex ├── interface_nakama_1_1_i_api_read_storage_object_id.eps ├── interface_nakama_1_1_i_api_read_storage_object_id.tex ├── interface_nakama_1_1_i_api_read_storage_objects_request.tex ├── interface_nakama_1_1_i_api_rpc.tex ├── interface_nakama_1_1_i_api_session.tex ├── interface_nakama_1_1_i_api_session_logout_request.tex ├── interface_nakama_1_1_i_api_session_refresh_request.tex ├── interface_nakama_1_1_i_api_storage_object.tex ├── interface_nakama_1_1_i_api_storage_object_ack.tex ├── interface_nakama_1_1_i_api_storage_object_acks.tex ├── interface_nakama_1_1_i_api_storage_object_list.tex ├── interface_nakama_1_1_i_api_storage_objects.tex ├── interface_nakama_1_1_i_api_subscription_list.tex ├── interface_nakama_1_1_i_api_tournament.tex ├── interface_nakama_1_1_i_api_tournament_list.tex ├── interface_nakama_1_1_i_api_tournament_record_list.tex ├── interface_nakama_1_1_i_api_update_account_request.tex ├── interface_nakama_1_1_i_api_update_group_request.tex ├── interface_nakama_1_1_i_api_user.tex ├── interface_nakama_1_1_i_api_user_group_list.tex ├── interface_nakama_1_1_i_api_users.tex ├── interface_nakama_1_1_i_api_validate_purchase_apple_request.tex ├── interface_nakama_1_1_i_api_validate_purchase_google_request.tex ├── interface_nakama_1_1_i_api_validate_purchase_huawei_request.tex ├── interface_nakama_1_1_i_api_validate_purchase_response.tex ├── interface_nakama_1_1_i_api_validate_subscription_apple_request.tex ├── interface_nakama_1_1_i_api_validate_subscription_google_request.tex ├── interface_nakama_1_1_i_api_validate_subscription_response.tex ├── interface_nakama_1_1_i_api_validated_purchase.tex ├── interface_nakama_1_1_i_api_validated_subscription.tex ├── interface_nakama_1_1_i_api_write_storage_object.eps ├── interface_nakama_1_1_i_api_write_storage_object.tex ├── interface_nakama_1_1_i_api_write_storage_objects_request.tex ├── interface_nakama_1_1_i_channel.tex ├── interface_nakama_1_1_i_channel_message_ack.tex ├── interface_nakama_1_1_i_channel_presence_event.tex ├── interface_nakama_1_1_i_client.eps ├── interface_nakama_1_1_i_client.tex ├── interface_nakama_1_1_i_group_user_list_group_user.tex ├── interface_nakama_1_1_i_http_adapter.eps ├── interface_nakama_1_1_i_http_adapter.tex ├── interface_nakama_1_1_i_logger.eps ├── interface_nakama_1_1_i_logger.tex ├── interface_nakama_1_1_i_match.tex ├── interface_nakama_1_1_i_match_presence_event.tex ├── interface_nakama_1_1_i_match_state.tex ├── interface_nakama_1_1_i_matchmaker_matched.tex ├── interface_nakama_1_1_i_matchmaker_ticket.tex ├── interface_nakama_1_1_i_matchmaker_user.tex ├── interface_nakama_1_1_i_party.tex ├── interface_nakama_1_1_i_party_close.tex ├── interface_nakama_1_1_i_party_data.tex ├── interface_nakama_1_1_i_party_join_request.tex ├── interface_nakama_1_1_i_party_leader.tex ├── interface_nakama_1_1_i_party_matchmaker_ticket.tex ├── interface_nakama_1_1_i_party_presence_event.tex ├── interface_nakama_1_1_i_protobuf_any.tex ├── interface_nakama_1_1_i_rpc_status.tex ├── interface_nakama_1_1_i_session.eps ├── interface_nakama_1_1_i_session.tex ├── interface_nakama_1_1_i_socket.eps ├── interface_nakama_1_1_i_socket.tex ├── interface_nakama_1_1_i_socket_adapter.eps ├── interface_nakama_1_1_i_socket_adapter.tex ├── interface_nakama_1_1_i_status.tex ├── interface_nakama_1_1_i_status_presence_event.tex ├── interface_nakama_1_1_i_stream.tex ├── interface_nakama_1_1_i_stream_presence_event.tex ├── interface_nakama_1_1_i_stream_state.tex ├── interface_nakama_1_1_i_user_group_list_user_group.tex ├── interface_nakama_1_1_i_user_presence.tex ├── interface_nakama_1_1_i_write_leaderboard_record_request_leaderboard_record_write.tex ├── interface_nakama_1_1_i_write_tournament_record_request_tournament_record_write.tex ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_buffer_pool.eps ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_buffer_pool.tex ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_ping_pong_manager.eps ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_ping_pong_manager.tex ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_client_factory.eps ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_client_factory.tex ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_server_factory.eps ├── interface_nakama_1_1_ninja_1_1_web_sockets_1_1_i_web_socket_server_factory.tex ├── interface_nakama_1_1_tests_1_1_i_nested_test_object.tex ├── interface_nakama_1_1_tests_1_1_i_test_object.tex ├── interface_satori_1_1_i_api_authenticate_logout_request.tex ├── interface_satori_1_1_i_api_authenticate_refresh_request.tex ├── interface_satori_1_1_i_api_authenticate_request.tex ├── interface_satori_1_1_i_api_event.tex ├── interface_satori_1_1_i_api_event_request.tex ├── interface_satori_1_1_i_api_experiment.tex ├── interface_satori_1_1_i_api_experiment_list.tex ├── interface_satori_1_1_i_api_flag.tex ├── interface_satori_1_1_i_api_flag_list.tex ├── interface_satori_1_1_i_api_identify_request.tex ├── interface_satori_1_1_i_api_live_event.tex ├── interface_satori_1_1_i_api_live_event_list.tex ├── interface_satori_1_1_i_api_properties.tex ├── interface_satori_1_1_i_api_session.tex ├── interface_satori_1_1_i_api_update_properties_request.tex ├── interface_satori_1_1_i_client.eps ├── interface_satori_1_1_i_client.tex ├── interface_satori_1_1_i_http_adapter.eps ├── interface_satori_1_1_i_http_adapter.tex ├── interface_satori_1_1_i_logger.tex ├── interface_satori_1_1_i_protobuf_any.tex ├── interface_satori_1_1_i_rpc_status.tex ├── interface_satori_1_1_i_session.eps ├── interface_satori_1_1_i_session.tex ├── longtable_doxygen.sty ├── md__c_h_a_n_g_e_l_o_g.tex ├── md__r_e_a_d_m_e.tex ├── md__r_e_l_e_a_s_e_i_n_s_t.tex ├── md__satori__tests__r_e_a_d_m_e.tex ├── md_codegen__r_e_a_d_m_e.tex ├── namespace_nakama.tex ├── namespace_nakama_1_1_console.tex ├── namespace_nakama_1_1_ninja.tex ├── namespace_nakama_1_1_ninja_1_1_web_sockets.tex ├── namespace_nakama_1_1_ninja_1_1_web_sockets_1_1_exceptions.tex ├── namespace_nakama_1_1_ninja_1_1_web_sockets_1_1_internal.tex ├── namespace_nakama_1_1_tests.tex ├── namespace_nakama_1_1_tests_1_1_api.tex ├── namespace_nakama_1_1_tests_1_1_socket.tex ├── namespace_nakama_1_1_tiny_json.tex ├── namespace_satori.tex ├── namespace_satori_1_1_tests.tex ├── namespace_satori_1_1_tiny_json.tex ├── namespaces.tex ├── refman.tex └── tabu_doxygen.sty ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig, options: https://editorconfig.org root = true [*] end_of_line = lf insert_final_newline = true [*.{yml,yaml}] charset = utf-8 indent_style = space indent_size = 4 ================================================ FILE: .github/workflows/doxygen.yml ================================================ name: Generate Doxygen Docs on: push: branches: - master jobs: doxygen: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: mattnotmitt/doxygen-action@1.9.4 with: working-directory: './docs' doxyfile-path: 'Doxyfile' - uses: stefanzweifel/git-auto-commit-action@v4.15.1 with: commit_message: Update Doxygen docs ================================================ FILE: .github/workflows/pr.yml ================================================ name: Checkout and Test on: pull_request: jobs: test: # disabled if: false runs-on: ubuntu-latest steps: - name: Checkout nakama-dotnet uses: actions/checkout@v2 with: ref: ${{ github.head_ref }} repository: heroiclabs/nakama-dotnet - name: Checkout nakama-client-testrunner uses: actions/checkout@v2 with: ref: ${{ github.head_ref }} repository: heroiclabs/nakama-client-testrunner - name: Start docker containers for nakama-client-testrunner working-directory: nakama-client-testrunner run: ./docker-compose up -d --wait - name: Run tests for nakama-dotnet working-directory: nakama-dotnet run: dotnet test Nakama.Tests ================================================ FILE: .gitignore ================================================ Nakama.sln.DotSettings .env .task/ # Created by https://www.gitignore.io/api/cake,linux,macos,csharp,windows,monodevelop,intellij+all,visualstudio,visualstudiocode # Edit at https://www.gitignore.io/?templates=cake,linux,macos,csharp,windows,monodevelop,intellij+all,visualstudio,visualstudiocode ### Cake ### tools/* !tools/packages.config ### Csharp ### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- Backup*.rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ ### Intellij+all ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/modules.xml # .idea/*.iml # .idea/modules # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser # JetBrains templates **___jb_tmp___ ### Intellij+all Patch ### # Ignores the whole .idea folder and all .iml files # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 .idea/ # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 *.iml modules.xml .idea/misc.xml *.ipr # Sonarlint plugin .idea/sonarlint ### Linux ### # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### MonoDevelop ### #User Specific *.usertasks #Mono Project Files *.resources test-results/ ### VisualStudioCode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json ### VisualStudioCode Patch ### # Ignore all local history of files .history ### Windows ### # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk ### VisualStudio ### # User-specific files # User-specific files (MonoDevelop/Xamarin Studio) # Mono auto generated files # Build results # Visual Studio 2015/2017 cache/options directory # Uncomment if you have tasks that create the project's static files in wwwroot # Visual Studio 2017 auto generated files # MSTest test Results # NUNIT # Build Results of an ATL Project # Benchmark Results # .NET Core # StyleCop # Files built by Visual Studio # Chutzpah Test files # Visual C++ cache files # Visual Studio profiler # Visual Studio Trace Files # TFS 2012 Local Workspace # Guidance Automation Toolkit # ReSharper is a .NET coding add-in # JustCode is a .NET coding add-in # TeamCity is a build add-in # DotCover is a Code Coverage Tool # AxoCover is a Code Coverage Tool # Visual Studio code coverage results # NCrunch # MightyMoose # Web workbench (sass) # Installshield output folder # DocProject is a documentation generator add-in # Click-Once directory # Publish Web Output # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted # NuGet Packages # The packages folder can be ignored because of Package Restore # except build/, which is used as an MSBuild target. # Uncomment if necessary however generally it will be regenerated when needed # NuGet v3's project.json files produces more ignorable files # Microsoft Azure Build Output # Microsoft Azure Emulator # Windows Store app package directories and files # Visual Studio cache files # files ending in .cache can be ignored # but keep track of directories ending in .cache # Others # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) # RIA/Silverlight projects # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) # SQL Server files # Business Intelligence projects # Microsoft Fakes # GhostDoc plugin setting file # Node.js Tools for Visual Studio # Visual Studio 6 build log # Visual Studio 6 workspace options file # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) # Visual Studio LightSwitch build output # Paket dependency manager # FAKE - F# Make # CodeRush personal settings # Python Tools for Visual Studio (PTVS) # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio # Telerik's JustMock configuration file # BizTalk build output # OpenCover UI analysis results # Azure Stream Analytics local run output # MSBuild Binary and Structured Log # NVidia Nsight GPU debugger configuration file # MFractors (Xamarin productivity tool) working folder # Local History for Visual Studio # BeatPulse healthcheck temp database # Backup folder for Package Reference Convert tool in Visual Studio 2017 # End of https://www.gitignore.io/api/cake,linux,macos,csharp,windows,monodevelop,intellij+all,visualstudio,visualstudiocode ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [3.21.2] - 2026-02-13 ### Changed - Nakama+Satori: Improve how HTTP requests are logged in the request adapter. ### Fixed - Satori: Fix argument order to client "GetFlagOverrides" function. - Nakama+Satori: Fix proto dependency resolver within build task. ## [3.21.1] - 2025-12-21 ### Fixed - Embed README into Nuget package distribution with correct file path. - Satori: Use correct argument order with GetFlags after code generator changes. ## [3.21.0] - 2025-12-07 ### Added - Satori: Add phase name and phase variant info to Experiment return types. - Satori: Add message IDs as optional filter input when Messages are listed. ### Changed - Nakama: Suppress WebSocket exception race condition on graceful socket closure with Unity/Mono runtime. - Nakama+Satori: Update various Nuget dependencies used in Test profile. ### Fixed - Nakama: Work around issue in Unity IL2CPP with broader use of Preserve annotation on stream types. ## [3.20.0] - 2025-10-01 ### Added - Nakama: New "ConsoleClient" functions to make HTTP requests to the Nakama Console. ### Fixed - Satori: Avoid recursive calls to GetFlags overload functions. ## [3.19.0] - 2025-09-29 ### Added - Satori: Enable logger to be passed into the "HttpRequestAdapter." - Satori: Update client with latest "JoinLiveEventAsync" feature. ### Changed - Use [Task](https://taskfile.dev/) as the task runner for codegen, build, and publish commands. - Pin the Protobuf plugins used to generate the Swagger spec used by the codegen tool. ## [3.18.0] - 2025-09-01 ### Changed - Nakama: If the server sends a close frame control message, gracefully respond with close output from the socket. - Satori: Make "Update" method public in Satori "Session" type. - Nakama+Satori: Expose "TimeoutException" when CTS timeout occurs within "HttpRequestAdapter." ## [3.17.0] - 2025-07-16 ### Added - Nakama: New Realtime Parties search feature which allows open parties to be discovered by users. ### Changed - Nakama+Satori: TinyJson will now encode any "IDictionary<,>" type. ### Fixed - Nakama: "ListStorageObjectsAsync" can use a user ID as input to fetch public storage objects for that user. Thanks @chrisanicolaou. - Nakama+Satori: Fix how issue at time is decoded from Session token. ## [3.16.0] - 2025-02-13 ### Added - Satori: Update the Client type with the newest Satori API. See [release notes](https://heroiclabs.com/docs/satori/concepts/introduction/release-notes/). ### Changed - Nakama+Satori: Embed version information based on Git describe into builds. ### Fixed - Nakama+Satori: Use formatted arguments with all logger lines in request adapters. - Satori: Use session token as jitter seed for randomized backoff with retries. - Nakama: "ISession.CreateTime" now accurately represents Unix time in seconds since the "ISession" object was created. ## [3.15.0] - 2025-01-28 ### Added - Satori: Add retry attempts to "Client" type. ### Changed - Nakama: Improve how cancellation is handled in HTTP requests. - Satori: Improve how cancellation is handled in HTTP requests. - Satori: Timeouts set in "Client" are now propagated to the underlying "ApiClient" type. ### Fixed - GitHub Pages documentation no longer uses Jekyll transform. ## [3.14.0] - 2024-10-20 ### Added - Satori: New "IClient" event called "ReceivedSessionUpdated" when session expires and is refreshed. ### Changed - Satori: The new session returned by "IdentifyAsync" is merged into the input "Session" type. ## [3.13.0] - 2024-07-10 ### Added - Satori: Added "ImageUrl" and "Title" to "IApiMessage". ## [3.12.1] - 2024-05-30 ### Fixed - Nakama: Fixed an issue where notifications from other users could appear as if they were sent by the recipient user. - Nakama: Fixed a potential "NullReferenceException" that could occur when passing a "null" username to "IClient.UpdateAccountAsync". ## [3.12.0] - 2024-04-08 ### Added - Satori: Added "IApiLiveEvent.Id" for accessing live event identifiers. - Satori: Added support for new Satori Messages API: "IClient.GetMessageListAsync", "IClient.UpdateMessageAsync" and "IClient.DeleteMessageAsync". ## [3.11.0] - 2024-03-08 ### Added - Nakama: New "IClient" event called "ReceivedSessionUpdated" when session expires and is refreshed. - Nakama: New "Session.Update" method to allow for in-place updates to the session object. ### Changed - Nakama: "IsConnected" and "IsConnecting" will now read directly from the underlying .NET socket status. This will allow application code to more quickly and easily detect connectivity loss arising from a lack of internet access. - Nakama: Default socket adapter changed from "WebSocketAdapter" to "WebSocketStdlibAdapter". This was done to utilize the native .NET Websocket library for improved stability and maintenance. ### Fixed - Nakama: Trying to connect a socket that is already connecting now results in a no-op instead of an error. ## [3.10.0] - 2023-11-21 ### Changed - Nakama: Mark socket as connected before event handler is called. - Nakama: Limited scope of retry logic to very specific 500-level codes from the server. ### Added - Nakama: Rank count is now returned with tournament record listings. - Nakama: Added ability to delete tournament records with "DeleteTournamentRecordAsync". - Nakama: Hostnames passed to the client now preserve their hardcoded paths. - Nakama: Create and update times are now returned with notifications. - Nakama: Added Facebook Instant Games purchase validation. ## [3.9.0] ### Added - Satori: Added "recompute" option to "UpdatePropertiesAsync" which allows audiences to be recalculated on property update. ### Changed - Satori: Decreased size of JSON payloads. ### Fixed - Satori: "DeleteIdentityAsync" no longer accepts an explicit ID. ## [3.8.0] ### Added - Nakama: Added "Authoritative" flag to tournaments returned from the server. - Nakama: Added "RefundTime" and "UserId" to purchases and subscriptions returned from the server. - Nakama: Added raw subscription provider information. - Nakama: Added "DeleteAccountAsync" for deleting user accounts. - Satori: Added "DeleteIdentityAsync" for deleting user identities. ### Changed - Nakama: Used "session.Username" wherever outdated state might be returned. ### Fixed - Nakama: Fixed issue where outgoing payloads could include unnecessary JSON. ## [3.7.0] ### Added - Nakama: Added a "UpdatePresences" utility to "IMatch" and "IParty". Use this method to maintain the presences in your matches and parties when an "IMatchPresenceEvent" or "IPartyPresenceEvent" is dispatched. - Satori: Added optional default and custom properties that can be attached to authentication requests. ### Changed - Satori: "GetFlagDefault" and "GetFlagsDefault" now use the "apiKey" passed to the client constructor rather than accepting it as a unique parameter. ## [3.6.0] ### Added - Satori: Adds the Satori .NET SDK. Satori is our liveops server for game studios. Please read more about it on the Heroic Labs website. - Nakama: Adds support for calling RPCs with a HTTP key via POST when a payload is provided. - Nakama: Expose the "Logger" object on "IClient". - Nakama: Adds support for POST RPC requests when using HTTP key with a payload ### Fixed - Nakama: Prevent race condition when "Close" is called while receive loop has an incomplete read buffer. - Nakama: Fixed an issue where 500 errors could cause parsing issues on the client. - Nakama: Added ability to specify "path" parameter to client urls. ### Changed - Nakama: Fixed an issue where our websocket would throw an exception on "CloseAsync()" in certain situations. ## [3.5.0] - 2022-09-06 ### Added - Ability to "persist" Apple, Huawei, and Google purchase receipts in the Nakama database. This is set to "true" by default in order to allow the server to detect replay attacks. - Added a "SeenBefore" property to "IApiValidatedPurchase". - Added "ListSubscriptionsAsync" which returns a list of the user's subscriptions. - Added "ValidateSubscriptionAppleAsync" which returns details about a user's Apple subscription. - Added "ValidateSubscriptionGoogleAsync" which returns details about a user's Google subscription. - Added "GetSubscriptionAsync" which returns a subscription for the provided product id. - Added support for "countMultiple" in "AddMatchmakerAsync" and "AddMatchmakerPartyAsync". ### Changed - "ValidatedPurchaseEnvironment" has been renamed to "ApiStoreEnvironment". - "ValidatedPurchaseStore" has been renamed to "ApiStoreProvider". - Removed obsolete client methods that accept a "CancellationTokenSource". These have been replaced in favor of methods that accept a "CancellationToken" that were added in v3.3. ### Fixed - Fixed an issue with Socket Closed event taking a significant length of time or not firing at all when internet connection is lost. - Fixed an issue with "SocketClosed" event taking a significant length of time or not firing at all when internet connection is lost. - Fixed an issue that would occur when sending messages over the socket from multiple threads. - Fixed automatic retry seeding to be random across devices. - Fixed an issue when parsing unquoted numbers as strings in TinyJson. ## [3.4.0] - 2022-04-28 ### Added - Allow max message size limit with socket messages to be overridden in the adapter. - Relayed multiplayer matches can now be created with a custom name (i.e. room name). ### Fixed - Fix background read loop to update 'IsConnecting' and 'IsConnected' when close is detected. ## [3.3.0] - 2022-01-24 ### Added - Add overload methods in Client which take a CancellationToken. Thanks @gamecentric. - Add WebSocketStdlibAdapter allows the codebase to be used in WASM and Blazor projects. Thanks @mattkanwisher. ### Changed - Use DualMode in TcpClient to handle NAT64 overlay networks (some mobile carriers). - Refactor the socket adapter design to use Tasks (previously avoided for Unity WebGL compat.). - Socket messages which exceed the internal buffer size now generate an "InternalBufferOverflowException" type. - A socket connect made on an already connected socket will no longer raise an exception. - Propagate up the "WebSocketException" type thrown on socket messages sent over a disconnected socket. - Update bundled "Ninja.WebSockets" library to commit 0b698a733f0e8711da7a5854154fe7d8a01fbd06. ### Fixed - Expose base exception if retry handler fails. ## [3.2.0] - 2021-10-11 ### Added - Added additional group listing filters. - Added ability to overwrite leaderboard/tournament ranking operators from the client. ### Fixed - Fixed url-safe encoding of query params that were passed to the client as arrays of strings. ## [3.1.1] - 2021-08-19 ### Changed - Removed "autoRefreshSession" from overloaded "Client" constructors. This can still be customized with the base "Client" constructor. This is a workaround for an internal compiler error in Unity's WebGL toolchain. ## [3.1.0] - 2021-08-11 ### Added - Added ability for user to retry requests if they fail due to a transient network error. - Added ability for user to cancel requests that are in-flight. ## [3.0.0] - 2021-07-14 ### Added - The language tag for the user can be configured with the socket on connect. ### Changed - An "IPartyMatchmakerTicket" is now received by the party leader when they add their party to the matchmaker via "AddMatchmakerPartyAsync". - Renamed "PromotePartyMember" to "PromotePartyMemberAsync". ## [2.9.3] - 2021-06-17 ### Fixed - Fixed issue where refreshing a session with metadata threw an exception due to the key already existing. ## [2.9.2] - 2021-05-21 ### Fixed - Fixed issue where "IUserPresence" objects were not being deserialized properly by the client as part of the "IParty" object. ### Changed - AddMatchmakerPartyAsync now returns an IPartyMatchmakerTicket. - Renamed PromotePartyMember to PromotePartyMemberAsync. ## [2.9.1] - 2021-05-19 ### Added - The "Socket.ReceivedParty" event can now be subscribed to in order to listen for acceptance events from the leader of a closed party. ## [2.9.0] - 2021-05-15 ### Added - A session can be refreshed on demand with "SessionRefreshAsync" method. - Session and/or refresh tokens can now be disabled with a client logout. - The client now supports session auto-refresh using refresh tokens. This is enabled by default. - New socket RPC and MatchSend methods using ArraySegment to allow developers to manage memory re-use. - Add IAP validation APIs for purchase receipts with Apple App Store, Google Play Store, and Huawei AppGallery. - Add Realtime Parties feature. ### Changed - Use lock object with socket operations instead of ConcurrentDictionary as a workaround for a Unity engine WebGL regression. - Avoid use of extension methods as a workaround for a Unity engine WebGL regression. ### Fixed - Parse HTTP responses defensively in case of bad load balancer configurations. ## [2.8.0] - 2020-02-19 ### Changed - Listing tournaments can now be done without providing start or end time filters. - Can now import Steam friends after authenticating or linking to a Steam account. ## [2.7.1] - 2020-02-1 ### Fixed - HTTP Client now properly reads off timeout value. ## [2.7.0] - 2020-10-19 ### Changed - Upgrade code generator to new Swagger format. ### Fixed - Properly pass server key to Apple auth calls. ## [2.6.0] - 2020-09-21 ### Added - Added Apple single sign-on support. - Added Steam single sign-on support. ### Fixed - Fixed serialization of HTTP API error messages. ### Changed - Silenced a noisy but benign exception related to web socket connections. ## [2.5.0] - 2020-08-12 ### Added - Add parsing support for the Nakama Console API to the code generator. - Add support for emitting custom events to the Nakama server. - Add ban and demote API to the client. ### Changed - Update TinyJson packaged dependency to the '01c586d' commit. - Remove usage of "System.Diagnostic.Tracing" from the codebase. This improves compatibility with Unity engine. - Use a Preserve annotation to mark fields which should not be code stripped at build time. This improves compatibility with Unity engine. ## [2.4.0] - 2020-05-04 :star: ### Added - New ListStorageObjectsAsync method and marked ListStorageObjects as obsolete. ### Changed - ListUsersStorageObjectsAsync now uses default arguments for optional inputs. ### Fixed - Prevent InvalidOperationException caused when socket connect task is already completed. ## [2.3.1] - 2019-09-21 ### Changed - Use workaround for IPv6 bug in TcpClient with Mono runtime used with Unity engine. ### Fixed - Add missing metadata to match join message. - Add discrete channel identifier in all channel related messages. ## [2.3.0] - 2019-09-02 ### Added - Follow users by username for status updates. - Decode session variables from the auth token. - Paginate friends, groups, and user's group listings. - Filter friends, groups, and user's group listings. - Send session variables with authenticate requests. - Socket messages now use a send timeout of 15 seconds to write to the buffer. ### Changed - Increase the default socket timeout to 30 seconds. ### Fixed - Use the connect timeout value in native socket connect attempts. - Link the token source across socket connect and close tasks. ## [2.2.2] - 2019-07-02 ### Changed - Don't synchronize the socket receive with the current thread context. - Remove workaround for Mono runtime usage with newer TLS negotation. ### Fixed - Resolve deadlock in socket dispose with synchronization context. ## [2.2.1] - 2019-06-19 ### Added - New comparison methods on some domain types. ### Changed - When an auth token is decoded into a session but is null or empty now return null. ### Fixed - Awaited socket callback tasks are now canceled when the socket adapter is closed and cleared. - Awaited socket callback tasks are now canceled when the socket adapter sends while disconnected. - Restored missing helper object with storage writes. ## [2.2.0] - 2019-06-06 ### Added - Add tournaments API. - Add leaderboards around owner API. - Provide more overload methods to the socket object for simpler usage. ### Changed - Update TinyJson packaged dependency to latest version. - Replace WebSocketListener with a new socket library. - Flatten use of Tasks in method responses. ### Fixed - Logger is now initialized correctly with socket debugging. - Stream data state is correctly deserialized from socket messages. - Fix callback ID on chat and match leave messages. ## [2.1.0] - 2018-08-17 ### Added - Detect socket message encodings. - All authenticate methods can now pass in username and create options. - Support gzip compress/decompress on ApiClient methods. ### Changed - Update the code generator to handle POST/DELETE query params. - Match listings can now pass through "null" to indicate no filters. - ApiClient exceptions now contain HTTP status codes. - Update lowlevel websocket driver due to performance issues on AOT targets like iOS with Unity. - Disable request decompression by default due to Unity+Android issue. ### Fixed - Reuse the HTTP client across all methods. ## [2.0.0] - 2018-06-18 ### Added - Initial public release. This version starts at 2.0 to match the initial server version it supports. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Nakama/ApiClient.gen.cs ================================================ /* Code generated by codegen/main.go. DO NOT EDIT. */ namespace Nakama { using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; using TinyJson; /// /// An exception generated for HttpResponse objects don't return a success status. /// public sealed class ApiResponseException : Exception { public long StatusCode { get; } public int GrpcStatusCode { get; } public ApiResponseException(long statusCode, string content, int grpcCode) : base(content) { StatusCode = statusCode; GrpcStatusCode = grpcCode; } public ApiResponseException(string message, Exception e) : base(message, e) { StatusCode = -1L; GrpcStatusCode = -1; } public ApiResponseException(string content) : this(-1L, content, -1) { } public override string ToString() { return $"ApiResponseException(StatusCode={StatusCode}, Message='{Message}', GrpcStatusCode={GrpcStatusCode})"; } } /// /// Update fields in a given group. /// public interface IApiUpdateGroupRequest { /// /// Avatar URL. /// string AvatarUrl { get; } /// /// Description string. /// string Description { get; } /// /// Lang tag. /// string LangTag { get; } /// /// Name. /// string Name { get; } /// /// Open is true if anyone should be allowed to join, or false if joins must be approved by a group admin. /// bool Open { get; } } /// internal class ApiUpdateGroupRequest : IApiUpdateGroupRequest { /// [DataMember(Name="avatar_url"), Preserve] public string AvatarUrl { get; set; } /// [DataMember(Name="description"), Preserve] public string Description { get; set; } /// [DataMember(Name="lang_tag"), Preserve] public string LangTag { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="open"), Preserve] public bool Open { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AvatarUrl: ", AvatarUrl, ", "); output = string.Concat(output, "Description: ", Description, ", "); output = string.Concat(output, "LangTag: ", LangTag, ", "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "Open: ", Open, ", "); return output; } } /// /// A friend of a friend. /// public interface IFriendsOfFriendsListFriendOfFriend { /// /// The user who referred its friend. /// string Referrer { get; } /// /// User. /// IApiUser User { get; } } /// internal class FriendsOfFriendsListFriendOfFriend : IFriendsOfFriendsListFriendOfFriend { /// [DataMember(Name="referrer"), Preserve] public string Referrer { get; set; } /// [IgnoreDataMember] public IApiUser User => _user; [DataMember(Name="user"), Preserve] public ApiUser _user { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Referrer: ", Referrer, ", "); output = string.Concat(output, "User: ", User, ", "); return output; } } /// /// A single user-role pair. /// public interface IGroupUserListGroupUser { /// /// Their relationship to the group. /// int State { get; } /// /// User. /// IApiUser User { get; } } /// internal class GroupUserListGroupUser : IGroupUserListGroupUser { /// [DataMember(Name="state"), Preserve] public int State { get; set; } /// [IgnoreDataMember] public IApiUser User => _user; [DataMember(Name="user"), Preserve] public ApiUser _user { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "State: ", State, ", "); output = string.Concat(output, "User: ", User, ", "); return output; } } /// /// A single group-role pair. /// public interface IUserGroupListUserGroup { /// /// Group. /// IApiGroup Group { get; } /// /// The user's relationship to the group. /// int State { get; } } /// internal class UserGroupListUserGroup : IUserGroupListUserGroup { /// [IgnoreDataMember] public IApiGroup Group => _group; [DataMember(Name="group"), Preserve] public ApiGroup _group { get; set; } /// [DataMember(Name="state"), Preserve] public int State { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Group: ", Group, ", "); output = string.Concat(output, "State: ", State, ", "); return output; } } /// /// Record values to write. /// public interface IWriteLeaderboardRecordRequestLeaderboardRecordWrite { /// /// Optional record metadata. /// string Metadata { get; } /// /// Operator override. /// ApiOperator Operator { get; } /// /// The score value to submit. /// string Score { get; } /// /// An optional secondary value. /// string Subscore { get; } } /// internal class WriteLeaderboardRecordRequestLeaderboardRecordWrite : IWriteLeaderboardRecordRequestLeaderboardRecordWrite { /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [IgnoreDataMember] public ApiOperator Operator => _operator; [DataMember(Name="operator"), Preserve] public ApiOperator _operator { get; set; } /// [DataMember(Name="score"), Preserve] public string Score { get; set; } /// [DataMember(Name="subscore"), Preserve] public string Subscore { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "Operator: ", Operator, ", "); output = string.Concat(output, "Score: ", Score, ", "); output = string.Concat(output, "Subscore: ", Subscore, ", "); return output; } } /// /// Record values to write. /// public interface IWriteTournamentRecordRequestTournamentRecordWrite { /// /// A JSON object of additional properties (optional). /// string Metadata { get; } /// /// Operator override. /// ApiOperator Operator { get; } /// /// The score value to submit. /// string Score { get; } /// /// An optional secondary value. /// string Subscore { get; } } /// internal class WriteTournamentRecordRequestTournamentRecordWrite : IWriteTournamentRecordRequestTournamentRecordWrite { /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [IgnoreDataMember] public ApiOperator Operator => _operator; [DataMember(Name="operator"), Preserve] public ApiOperator _operator { get; set; } /// [DataMember(Name="score"), Preserve] public string Score { get; set; } /// [DataMember(Name="subscore"), Preserve] public string Subscore { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "Operator: ", Operator, ", "); output = string.Concat(output, "Score: ", Score, ", "); output = string.Concat(output, "Subscore: ", Subscore, ", "); return output; } } /// /// A user with additional account details. Always the current user. /// public interface IApiAccount { /// /// The custom id in the user's account. /// string CustomId { get; } /// /// The devices which belong to the user's account. /// IEnumerable Devices { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user's account was disabled/banned. /// string DisableTime { get; } /// /// The email address of the user. /// string Email { get; } /// /// The user object. /// IApiUser User { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user's email was verified. /// string VerifyTime { get; } /// /// The user's wallet data. /// string Wallet { get; } } /// internal class ApiAccount : IApiAccount { /// [DataMember(Name="custom_id"), Preserve] public string CustomId { get; set; } /// [IgnoreDataMember] public IEnumerable Devices => _devices ?? new List(0); [DataMember(Name="devices"), Preserve] public List _devices { get; set; } /// [DataMember(Name="disable_time"), Preserve] public string DisableTime { get; set; } /// [DataMember(Name="email"), Preserve] public string Email { get; set; } /// [IgnoreDataMember] public IApiUser User => _user; [DataMember(Name="user"), Preserve] public ApiUser _user { get; set; } /// [DataMember(Name="verify_time"), Preserve] public string VerifyTime { get; set; } /// [DataMember(Name="wallet"), Preserve] public string Wallet { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CustomId: ", CustomId, ", "); output = string.Concat(output, "Devices: [", string.Join(", ", Devices), "], "); output = string.Concat(output, "DisableTime: ", DisableTime, ", "); output = string.Concat(output, "Email: ", Email, ", "); output = string.Concat(output, "User: ", User, ", "); output = string.Concat(output, "VerifyTime: ", VerifyTime, ", "); output = string.Concat(output, "Wallet: ", Wallet, ", "); return output; } } /// /// Send a Apple Sign In token to the server. Used with authenticate/link/unlink. /// public interface IApiAccountApple { /// /// The ID token received from Apple to validate. /// string Token { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountApple : IApiAccountApple { /// [DataMember(Name="token"), Preserve] public string Token { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Token: ", Token, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// Send a custom ID to the server. Used with authenticate/link/unlink. /// public interface IApiAccountCustom { /// /// A custom identifier. /// string Id { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountCustom : IApiAccountCustom { /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Id: ", Id, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// Send a device to the server. Used with authenticate/link/unlink and user. /// public interface IApiAccountDevice { /// /// A device identifier. Should be obtained by a platform-specific device API. /// string Id { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountDevice : IApiAccountDevice { /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Id: ", Id, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// Send an email with password to the server. Used with authenticate/link/unlink. /// public interface IApiAccountEmail { /// /// A valid RFC-5322 email address. /// string Email { get; } /// /// A password for the user account. Ignored with unlink operations. /// string Password { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountEmail : IApiAccountEmail { /// [DataMember(Name="email"), Preserve] public string Email { get; set; } /// [DataMember(Name="password"), Preserve] public string Password { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Email: ", Email, ", "); output = string.Concat(output, "Password: ", Password, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// Send a Facebook token to the server. Used with authenticate/link/unlink. /// public interface IApiAccountFacebook { /// /// The OAuth token received from Facebook to access their profile API. /// string Token { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountFacebook : IApiAccountFacebook { /// [DataMember(Name="token"), Preserve] public string Token { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Token: ", Token, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// Send a Facebook Instant Game token to the server. Used with authenticate/link/unlink. /// public interface IApiAccountFacebookInstantGame { /// /// The OAuth token received from a Facebook Instant Game that may be decoded with the Application Secret (must be available with the nakama configuration) /// string SignedPlayerInfo { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountFacebookInstantGame : IApiAccountFacebookInstantGame { /// [DataMember(Name="signed_player_info"), Preserve] public string SignedPlayerInfo { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "SignedPlayerInfo: ", SignedPlayerInfo, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// Send Apple's Game Center account credentials to the server. Used with authenticate/link/unlink. https://developer.apple.com/documentation/gamekit/gklocalplayer/1515407-generateidentityverificationsign /// public interface IApiAccountGameCenter { /// /// Bundle ID (generated by GameCenter). /// string BundleId { get; } /// /// Player ID (generated by GameCenter). /// string PlayerId { get; } /// /// The URL for the public encryption key. /// string PublicKeyUrl { get; } /// /// A random "NSString" used to compute the hash and keep it randomized. /// string Salt { get; } /// /// The verification signature data generated. /// string Signature { get; } /// /// Time since UNIX epoch when the signature was created. /// string TimestampSeconds { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountGameCenter : IApiAccountGameCenter { /// [DataMember(Name="bundle_id"), Preserve] public string BundleId { get; set; } /// [DataMember(Name="player_id"), Preserve] public string PlayerId { get; set; } /// [DataMember(Name="public_key_url"), Preserve] public string PublicKeyUrl { get; set; } /// [DataMember(Name="salt"), Preserve] public string Salt { get; set; } /// [DataMember(Name="signature"), Preserve] public string Signature { get; set; } /// [DataMember(Name="timestamp_seconds"), Preserve] public string TimestampSeconds { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "BundleId: ", BundleId, ", "); output = string.Concat(output, "PlayerId: ", PlayerId, ", "); output = string.Concat(output, "PublicKeyUrl: ", PublicKeyUrl, ", "); output = string.Concat(output, "Salt: ", Salt, ", "); output = string.Concat(output, "Signature: ", Signature, ", "); output = string.Concat(output, "TimestampSeconds: ", TimestampSeconds, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// Send a Google token to the server. Used with authenticate/link/unlink. /// public interface IApiAccountGoogle { /// /// The OAuth token received from Google to access their profile API. /// string Token { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountGoogle : IApiAccountGoogle { /// [DataMember(Name="token"), Preserve] public string Token { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Token: ", Token, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// Send a Steam token to the server. Used with authenticate/link/unlink. /// public interface IApiAccountSteam { /// /// The account token received from Steam to access their profile API. /// string Token { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountSteam : IApiAccountSteam { /// [DataMember(Name="token"), Preserve] public string Token { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Token: ", Token, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// A message sent on a channel. /// public interface IApiChannelMessage { /// /// The channel this message belongs to. /// string ChannelId { get; } /// /// The code representing a message type or category. /// int Code { get; } /// /// The content payload. /// string Content { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the message was created. /// string CreateTime { get; } /// /// The ID of the group, or an empty string if this message was not sent through a group channel. /// string GroupId { get; } /// /// The unique ID of this message. /// string MessageId { get; } /// /// True if the message was persisted to the channel's history, false otherwise. /// bool Persistent { get; } /// /// The name of the chat room, or an empty string if this message was not sent through a chat room. /// string RoomName { get; } /// /// Message sender, usually a user ID. /// string SenderId { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the message was last updated. /// string UpdateTime { get; } /// /// The ID of the first DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdOne { get; } /// /// The ID of the second DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdTwo { get; } /// /// The username of the message sender, if any. /// string Username { get; } } /// internal class ApiChannelMessage : IApiChannelMessage { /// [DataMember(Name="channel_id"), Preserve] public string ChannelId { get; set; } /// [DataMember(Name="code"), Preserve] public int Code { get; set; } /// [DataMember(Name="content"), Preserve] public string Content { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="group_id"), Preserve] public string GroupId { get; set; } /// [DataMember(Name="message_id"), Preserve] public string MessageId { get; set; } /// [DataMember(Name="persistent"), Preserve] public bool Persistent { get; set; } /// [DataMember(Name="room_name"), Preserve] public string RoomName { get; set; } /// [DataMember(Name="sender_id"), Preserve] public string SenderId { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id_one"), Preserve] public string UserIdOne { get; set; } /// [DataMember(Name="user_id_two"), Preserve] public string UserIdTwo { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ChannelId: ", ChannelId, ", "); output = string.Concat(output, "Code: ", Code, ", "); output = string.Concat(output, "Content: ", Content, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "GroupId: ", GroupId, ", "); output = string.Concat(output, "MessageId: ", MessageId, ", "); output = string.Concat(output, "Persistent: ", Persistent, ", "); output = string.Concat(output, "RoomName: ", RoomName, ", "); output = string.Concat(output, "SenderId: ", SenderId, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserIdOne: ", UserIdOne, ", "); output = string.Concat(output, "UserIdTwo: ", UserIdTwo, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// A list of channel messages, usually a result of a list operation. /// public interface IApiChannelMessageList { /// /// Cacheable cursor to list newer messages. Durable and designed to be stored, unlike next/prev cursors. /// string CacheableCursor { get; } /// /// A list of messages. /// IEnumerable Messages { get; } /// /// The cursor to send when retrieving the next page, if any. /// string NextCursor { get; } /// /// The cursor to send when retrieving the previous page, if any. /// string PrevCursor { get; } } /// internal class ApiChannelMessageList : IApiChannelMessageList { /// [DataMember(Name="cacheable_cursor"), Preserve] public string CacheableCursor { get; set; } /// [IgnoreDataMember] public IEnumerable Messages => _messages ?? new List(0); [DataMember(Name="messages"), Preserve] public List _messages { get; set; } /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CacheableCursor: ", CacheableCursor, ", "); output = string.Concat(output, "Messages: [", string.Join(", ", Messages), "], "); output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); return output; } } /// /// Create a group with the current user as owner. /// public interface IApiCreateGroupRequest { /// /// A URL for an avatar image. /// string AvatarUrl { get; } /// /// A description for the group. /// string Description { get; } /// /// The language expected to be a tag which follows the BCP-47 spec. /// string LangTag { get; } /// /// Maximum number of group members. /// int MaxCount { get; } /// /// A unique name for the group. /// string Name { get; } /// /// Mark a group as open or not where only admins can accept members. /// bool Open { get; } } /// internal class ApiCreateGroupRequest : IApiCreateGroupRequest { /// [DataMember(Name="avatar_url"), Preserve] public string AvatarUrl { get; set; } /// [DataMember(Name="description"), Preserve] public string Description { get; set; } /// [DataMember(Name="lang_tag"), Preserve] public string LangTag { get; set; } /// [DataMember(Name="max_count"), Preserve] public int MaxCount { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="open"), Preserve] public bool Open { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AvatarUrl: ", AvatarUrl, ", "); output = string.Concat(output, "Description: ", Description, ", "); output = string.Concat(output, "LangTag: ", LangTag, ", "); output = string.Concat(output, "MaxCount: ", MaxCount, ", "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "Open: ", Open, ", "); return output; } } /// /// Storage objects to delete. /// public interface IApiDeleteStorageObjectId { /// /// The collection which stores the object. /// string Collection { get; } /// /// The key of the object within the collection. /// string Key { get; } /// /// The version hash of the object. /// string Version { get; } } /// internal class ApiDeleteStorageObjectId : IApiDeleteStorageObjectId { /// [DataMember(Name="collection"), Preserve] public string Collection { get; set; } /// [DataMember(Name="key"), Preserve] public string Key { get; set; } /// [DataMember(Name="version"), Preserve] public string Version { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Collection: ", Collection, ", "); output = string.Concat(output, "Key: ", Key, ", "); output = string.Concat(output, "Version: ", Version, ", "); return output; } } /// /// Batch delete storage objects. /// public interface IApiDeleteStorageObjectsRequest { /// /// Batch of storage objects. /// IEnumerable ObjectIds { get; } } /// internal class ApiDeleteStorageObjectsRequest : IApiDeleteStorageObjectsRequest { /// [IgnoreDataMember] public IEnumerable ObjectIds => _objectIds ?? new List(0); [DataMember(Name="object_ids"), Preserve] public List _objectIds { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ObjectIds: [", string.Join(", ", ObjectIds), "], "); return output; } } /// /// Represents an event to be passed through the server to registered event handlers. /// public interface IApiEvent { /// /// True if the event came directly from a client call, false otherwise. /// bool External { get; } /// /// An event name, type, category, or identifier. /// string Name { get; } /// /// Arbitrary event property values. /// IDictionary Properties { get; } /// /// The time when the event was triggered. /// string Timestamp { get; } } /// internal class ApiEvent : IApiEvent { /// [DataMember(Name="external"), Preserve] public bool External { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [IgnoreDataMember] public IDictionary Properties => _properties ?? new Dictionary(); [DataMember(Name="properties"), Preserve] public Dictionary _properties { get; set; } /// [DataMember(Name="timestamp"), Preserve] public string Timestamp { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "External: ", External, ", "); output = string.Concat(output, "Name: ", Name, ", "); var propertiesString = ""; foreach (var kvp in Properties) { propertiesString = string.Concat(propertiesString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Properties: [" + propertiesString + "]"); output = string.Concat(output, "Timestamp: ", Timestamp, ", "); return output; } } /// /// A friend of a user. /// public interface IApiFriend { /// /// Metadata. /// string Metadata { get; } /// /// The friend status. one of "Friend.State". /// int State { get; } /// /// Time of the latest relationship update. /// string UpdateTime { get; } /// /// The user object. /// IApiUser User { get; } } /// internal class ApiFriend : IApiFriend { /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="state"), Preserve] public int State { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [IgnoreDataMember] public IApiUser User => _user; [DataMember(Name="user"), Preserve] public ApiUser _user { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "State: ", State, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "User: ", User, ", "); return output; } } /// /// A collection of zero or more friends of the user. /// public interface IApiFriendList { /// /// Cursor for the next page of results, if any. /// string Cursor { get; } /// /// The Friend objects. /// IEnumerable Friends { get; } } /// internal class ApiFriendList : IApiFriendList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable Friends => _friends ?? new List(0); [DataMember(Name="friends"), Preserve] public List _friends { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "Friends: [", string.Join(", ", Friends), "], "); return output; } } /// /// A List of friends of friends /// public interface IApiFriendsOfFriendsList { /// /// Cursor for the next page of results, if any. /// string Cursor { get; } /// /// User friends of friends. /// IEnumerable FriendsOfFriends { get; } } /// internal class ApiFriendsOfFriendsList : IApiFriendsOfFriendsList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable FriendsOfFriends => _friendsOfFriends ?? new List(0); [DataMember(Name="friends_of_friends"), Preserve] public List _friendsOfFriends { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "FriendsOfFriends: [", string.Join(", ", FriendsOfFriends), "], "); return output; } } /// /// A group in the server. /// public interface IApiGroup { /// /// A URL for an avatar image. /// string AvatarUrl { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the group was created. /// string CreateTime { get; } /// /// The id of the user who created the group. /// string CreatorId { get; } /// /// A description for the group. /// string Description { get; } /// /// The current count of all members in the group. /// int EdgeCount { get; } /// /// The id of a group. /// string Id { get; } /// /// The language expected to be a tag which follows the BCP-47 spec. /// string LangTag { get; } /// /// The maximum number of members allowed. /// int MaxCount { get; } /// /// Additional information stored as a JSON object. /// string Metadata { get; } /// /// The unique name of the group. /// string Name { get; } /// /// Anyone can join open groups, otherwise only admins can accept members. /// bool Open { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the group was last updated. /// string UpdateTime { get; } } /// internal class ApiGroup : IApiGroup { /// [DataMember(Name="avatar_url"), Preserve] public string AvatarUrl { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="creator_id"), Preserve] public string CreatorId { get; set; } /// [DataMember(Name="description"), Preserve] public string Description { get; set; } /// [DataMember(Name="edge_count"), Preserve] public int EdgeCount { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="lang_tag"), Preserve] public string LangTag { get; set; } /// [DataMember(Name="max_count"), Preserve] public int MaxCount { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="open"), Preserve] public bool Open { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AvatarUrl: ", AvatarUrl, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "CreatorId: ", CreatorId, ", "); output = string.Concat(output, "Description: ", Description, ", "); output = string.Concat(output, "EdgeCount: ", EdgeCount, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "LangTag: ", LangTag, ", "); output = string.Concat(output, "MaxCount: ", MaxCount, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "Open: ", Open, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); return output; } } /// /// One or more groups returned from a listing operation. /// public interface IApiGroupList { /// /// A cursor used to get the next page. /// string Cursor { get; } /// /// One or more groups. /// IEnumerable Groups { get; } } /// internal class ApiGroupList : IApiGroupList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable Groups => _groups ?? new List(0); [DataMember(Name="groups"), Preserve] public List _groups { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "Groups: [", string.Join(", ", Groups), "], "); return output; } } /// /// A list of users belonging to a group, along with their role. /// public interface IApiGroupUserList { /// /// Cursor for the next page of results, if any. /// string Cursor { get; } /// /// User-role pairs for a group. /// IEnumerable GroupUsers { get; } } /// internal class ApiGroupUserList : IApiGroupUserList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable GroupUsers => _groupUsers ?? new List(0); [DataMember(Name="group_users"), Preserve] public List _groupUsers { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "GroupUsers: [", string.Join(", ", GroupUsers), "], "); return output; } } /// /// Represents a complete leaderboard record with all scores and associated metadata. /// public interface IApiLeaderboardRecord { /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard record was created. /// string CreateTime { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard record expires. /// string ExpiryTime { get; } /// /// The ID of the leaderboard this score belongs to. /// string LeaderboardId { get; } /// /// The maximum number of score updates allowed by the owner. /// int MaxNumScore { get; } /// /// Metadata. /// string Metadata { get; } /// /// The number of submissions to this score record. /// int NumScore { get; } /// /// The ID of the score owner, usually a user or group. /// string OwnerId { get; } /// /// The rank of this record. /// string Rank { get; } /// /// The score value. /// string Score { get; } /// /// An optional subscore value. /// string Subscore { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard record was updated. /// string UpdateTime { get; } /// /// The username of the score owner, if the owner is a user. /// string Username { get; } } /// internal class ApiLeaderboardRecord : IApiLeaderboardRecord { /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="expiry_time"), Preserve] public string ExpiryTime { get; set; } /// [DataMember(Name="leaderboard_id"), Preserve] public string LeaderboardId { get; set; } /// [DataMember(Name="max_num_score"), Preserve] public int MaxNumScore { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="num_score"), Preserve] public int NumScore { get; set; } /// [DataMember(Name="owner_id"), Preserve] public string OwnerId { get; set; } /// [DataMember(Name="rank"), Preserve] public string Rank { get; set; } /// [DataMember(Name="score"), Preserve] public string Score { get; set; } /// [DataMember(Name="subscore"), Preserve] public string Subscore { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "ExpiryTime: ", ExpiryTime, ", "); output = string.Concat(output, "LeaderboardId: ", LeaderboardId, ", "); output = string.Concat(output, "MaxNumScore: ", MaxNumScore, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "NumScore: ", NumScore, ", "); output = string.Concat(output, "OwnerId: ", OwnerId, ", "); output = string.Concat(output, "Rank: ", Rank, ", "); output = string.Concat(output, "Score: ", Score, ", "); output = string.Concat(output, "Subscore: ", Subscore, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// A set of leaderboard records, may be part of a leaderboard records page or a batch of individual records. /// public interface IApiLeaderboardRecordList { /// /// The cursor to send when retrieving the next page, if any. /// string NextCursor { get; } /// /// A batched set of leaderboard records belonging to specified owners. /// IEnumerable OwnerRecords { get; } /// /// The cursor to send when retrieving the previous page, if any. /// string PrevCursor { get; } /// /// The total number of ranks available. /// string RankCount { get; } /// /// A list of leaderboard records. /// IEnumerable Records { get; } } /// internal class ApiLeaderboardRecordList : IApiLeaderboardRecordList { /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [IgnoreDataMember] public IEnumerable OwnerRecords => _ownerRecords ?? new List(0); [DataMember(Name="owner_records"), Preserve] public List _ownerRecords { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } /// [DataMember(Name="rank_count"), Preserve] public string RankCount { get; set; } /// [IgnoreDataMember] public IEnumerable Records => _records ?? new List(0); [DataMember(Name="records"), Preserve] public List _records { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "OwnerRecords: [", string.Join(", ", OwnerRecords), "], "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); output = string.Concat(output, "RankCount: ", RankCount, ", "); output = string.Concat(output, "Records: [", string.Join(", ", Records), "], "); return output; } } /// /// Link Steam to the current user's account. /// public interface IApiLinkSteamRequest { /// /// The Facebook account details. /// IApiAccountSteam Account { get; } /// /// Import Steam friends for the user. /// bool Sync { get; } } /// internal class ApiLinkSteamRequest : IApiLinkSteamRequest { /// [IgnoreDataMember] public IApiAccountSteam Account => _account; [DataMember(Name="account"), Preserve] public ApiAccountSteam _account { get; set; } /// [DataMember(Name="sync"), Preserve] public bool Sync { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Account: ", Account, ", "); output = string.Concat(output, "Sync: ", Sync, ", "); return output; } } /// /// List user subscriptions. /// public interface IApiListSubscriptionsRequest { /// /// Cursor to retrieve a page of records from /// string Cursor { get; } /// /// Max number of results per page /// int Limit { get; } } /// internal class ApiListSubscriptionsRequest : IApiListSubscriptionsRequest { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [DataMember(Name="limit"), Preserve] public int Limit { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "Limit: ", Limit, ", "); return output; } } /// /// Represents a realtime match. /// public interface IApiMatch { /// /// True if it's an server-managed authoritative match, false otherwise. /// bool Authoritative { get; } /// /// Handler name /// string HandlerName { get; } /// /// Match label, if any. /// string Label { get; } /// /// The ID of the match, can be used to join. /// string MatchId { get; } /// /// Current number of users in the match. /// int Size { get; } /// /// Tick Rate /// int TickRate { get; } } /// internal class ApiMatch : IApiMatch { /// [DataMember(Name="authoritative"), Preserve] public bool Authoritative { get; set; } /// [DataMember(Name="handler_name"), Preserve] public string HandlerName { get; set; } /// [DataMember(Name="label"), Preserve] public string Label { get; set; } /// [DataMember(Name="match_id"), Preserve] public string MatchId { get; set; } /// [DataMember(Name="size"), Preserve] public int Size { get; set; } /// [DataMember(Name="tick_rate"), Preserve] public int TickRate { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Authoritative: ", Authoritative, ", "); output = string.Concat(output, "HandlerName: ", HandlerName, ", "); output = string.Concat(output, "Label: ", Label, ", "); output = string.Concat(output, "MatchId: ", MatchId, ", "); output = string.Concat(output, "Size: ", Size, ", "); output = string.Concat(output, "TickRate: ", TickRate, ", "); return output; } } /// /// A list of realtime matches. /// public interface IApiMatchList { /// /// A number of matches corresponding to a list operation. /// IEnumerable Matches { get; } } /// internal class ApiMatchList : IApiMatchList { /// [IgnoreDataMember] public IEnumerable Matches => _matches ?? new List(0); [DataMember(Name="matches"), Preserve] public List _matches { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Matches: [", string.Join(", ", Matches), "], "); return output; } } /// /// Matchmaker ticket completion stats /// public interface IApiMatchmakerCompletionStats { /// /// /// string CompleteTime { get; } /// /// /// string CreateTime { get; } } /// internal class ApiMatchmakerCompletionStats : IApiMatchmakerCompletionStats { /// [DataMember(Name="complete_time"), Preserve] public string CompleteTime { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CompleteTime: ", CompleteTime, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); return output; } } /// /// Matchmaker stats /// public interface IApiMatchmakerStats { /// /// /// IEnumerable Completions { get; } /// /// /// string OldestTicketCreateTime { get; } /// /// /// int TicketCount { get; } } /// internal class ApiMatchmakerStats : IApiMatchmakerStats { /// [IgnoreDataMember] public IEnumerable Completions => _completions ?? new List(0); [DataMember(Name="completions"), Preserve] public List _completions { get; set; } /// [DataMember(Name="oldest_ticket_create_time"), Preserve] public string OldestTicketCreateTime { get; set; } /// [DataMember(Name="ticket_count"), Preserve] public int TicketCount { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Completions: [", string.Join(", ", Completions), "], "); output = string.Concat(output, "OldestTicketCreateTime: ", OldestTicketCreateTime, ", "); output = string.Concat(output, "TicketCount: ", TicketCount, ", "); return output; } } /// /// A notification in the server. /// public interface IApiNotification { /// /// Category code for this notification. /// int Code { get; } /// /// Content of the notification in JSON. /// string Content { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the notification was created. /// string CreateTime { get; } /// /// ID of the Notification. /// string Id { get; } /// /// True if this notification was persisted to the database. /// bool Persistent { get; } /// /// ID of the sender, if a user. Otherwise 'null'. /// string SenderId { get; } /// /// Subject of the notification. /// string Subject { get; } } /// internal class ApiNotification : IApiNotification { /// [DataMember(Name="code"), Preserve] public int Code { get; set; } /// [DataMember(Name="content"), Preserve] public string Content { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="persistent"), Preserve] public bool Persistent { get; set; } /// [DataMember(Name="sender_id"), Preserve] public string SenderId { get; set; } /// [DataMember(Name="subject"), Preserve] public string Subject { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Code: ", Code, ", "); output = string.Concat(output, "Content: ", Content, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "Persistent: ", Persistent, ", "); output = string.Concat(output, "SenderId: ", SenderId, ", "); output = string.Concat(output, "Subject: ", Subject, ", "); return output; } } /// /// A collection of zero or more notifications. /// public interface IApiNotificationList { /// /// Use this cursor to paginate notifications. Cache this to catch up to new notifications. /// string CacheableCursor { get; } /// /// Collection of notifications. /// IEnumerable Notifications { get; } } /// internal class ApiNotificationList : IApiNotificationList { /// [DataMember(Name="cacheable_cursor"), Preserve] public string CacheableCursor { get; set; } /// [IgnoreDataMember] public IEnumerable Notifications => _notifications ?? new List(0); [DataMember(Name="notifications"), Preserve] public List _notifications { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CacheableCursor: ", CacheableCursor, ", "); output = string.Concat(output, "Notifications: [", string.Join(", ", Notifications), "], "); return output; } } /// /// /// public enum ApiOperator { /// /// Operator that can be used to override the one set in the leaderboard. /// NO_OVERRIDE = 0, /// /// /// BEST = 1, /// /// - NO_OVERRIDE: Do not override the leaderboard operator. /// SET = 2, /// /// - BEST: Override the leaderboard operator with BEST. /// INCREMENT = 3, /// /// - SET: Override the leaderboard operator with SET. /// DECREMENT = 4, } /// /// Incoming information about a party. /// public interface IApiParty { /// /// Hidden flag. /// bool Hidden { get; } /// /// The party label, if any. /// string Label { get; } /// /// Maximum number of party members. /// int MaxSize { get; } /// /// Open flag. /// bool Open { get; } /// /// Unique party identifier. /// string PartyId { get; } } /// internal class ApiParty : IApiParty { /// [DataMember(Name="hidden"), Preserve] public bool Hidden { get; set; } /// [DataMember(Name="label"), Preserve] public string Label { get; set; } /// [DataMember(Name="max_size"), Preserve] public int MaxSize { get; set; } /// [DataMember(Name="open"), Preserve] public bool Open { get; set; } /// [DataMember(Name="party_id"), Preserve] public string PartyId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Hidden: ", Hidden, ", "); output = string.Concat(output, "Label: ", Label, ", "); output = string.Concat(output, "MaxSize: ", MaxSize, ", "); output = string.Concat(output, "Open: ", Open, ", "); output = string.Concat(output, "PartyId: ", PartyId, ", "); return output; } } /// /// A list of realtime matches. /// public interface IApiPartyList { /// /// A cursor to send when retrieving the next page, if any. /// string Cursor { get; } /// /// A number of parties corresponding to a list operation. /// IEnumerable Parties { get; } } /// internal class ApiPartyList : IApiPartyList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable Parties => _parties ?? new List(0); [DataMember(Name="parties"), Preserve] public List _parties { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "Parties: [", string.Join(", ", Parties), "], "); return output; } } /// /// Storage objects to get. /// public interface IApiReadStorageObjectId { /// /// The collection which stores the object. /// string Collection { get; } /// /// The key of the object within the collection. /// string Key { get; } /// /// The user owner of the object. /// string UserId { get; } } /// internal class ApiReadStorageObjectId : IApiReadStorageObjectId { /// [DataMember(Name="collection"), Preserve] public string Collection { get; set; } /// [DataMember(Name="key"), Preserve] public string Key { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Collection: ", Collection, ", "); output = string.Concat(output, "Key: ", Key, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); return output; } } /// /// Batch get storage objects. /// public interface IApiReadStorageObjectsRequest { /// /// Batch of storage objects. /// IEnumerable ObjectIds { get; } } /// internal class ApiReadStorageObjectsRequest : IApiReadStorageObjectsRequest { /// [IgnoreDataMember] public IEnumerable ObjectIds => _objectIds ?? new List(0); [DataMember(Name="object_ids"), Preserve] public List _objectIds { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ObjectIds: [", string.Join(", ", ObjectIds), "], "); return output; } } /// /// Execute an Lua function on the server. /// public interface IApiRpc { /// /// The authentication key used when executed as a non-client HTTP request. /// string HttpKey { get; } /// /// The identifier of the function. /// string Id { get; } /// /// The payload of the function which must be a JSON object. /// string Payload { get; } } /// internal class ApiRpc : IApiRpc { /// [DataMember(Name="http_key"), Preserve] public string HttpKey { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="payload"), Preserve] public string Payload { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "HttpKey: ", HttpKey, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "Payload: ", Payload, ", "); return output; } } /// /// A user's session used to authenticate messages. /// public interface IApiSession { /// /// True if the corresponding account was just created, false otherwise. /// bool Created { get; } /// /// Refresh token that can be used for session token renewal. /// string RefreshToken { get; } /// /// Authentication credentials. /// string Token { get; } } /// internal class ApiSession : IApiSession { /// [DataMember(Name="created"), Preserve] public bool Created { get; set; } /// [DataMember(Name="refresh_token"), Preserve] public string RefreshToken { get; set; } /// [DataMember(Name="token"), Preserve] public string Token { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Created: ", Created, ", "); output = string.Concat(output, "RefreshToken: ", RefreshToken, ", "); output = string.Concat(output, "Token: ", Token, ", "); return output; } } /// /// Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. /// public interface IApiSessionLogoutRequest { /// /// Refresh token to invalidate. /// string RefreshToken { get; } /// /// Session token to log out. /// string Token { get; } } /// internal class ApiSessionLogoutRequest : IApiSessionLogoutRequest { /// [DataMember(Name="refresh_token"), Preserve] public string RefreshToken { get; set; } /// [DataMember(Name="token"), Preserve] public string Token { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "RefreshToken: ", RefreshToken, ", "); output = string.Concat(output, "Token: ", Token, ", "); return output; } } /// /// Authenticate against the server with a refresh token. /// public interface IApiSessionRefreshRequest { /// /// Refresh token. /// string Token { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiSessionRefreshRequest : IApiSessionRefreshRequest { /// [DataMember(Name="token"), Preserve] public string Token { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Token: ", Token, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// An object within the storage engine. /// public interface IApiStorageObject { /// /// The collection which stores the object. /// string Collection { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was created. /// string CreateTime { get; } /// /// The key of the object within the collection. /// string Key { get; } /// /// The read access permissions for the object. /// int PermissionRead { get; } /// /// The write access permissions for the object. /// int PermissionWrite { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was last updated. /// string UpdateTime { get; } /// /// The user owner of the object. /// string UserId { get; } /// /// The value of the object. /// string Value { get; } /// /// The version hash of the object. /// string Version { get; } } /// internal class ApiStorageObject : IApiStorageObject { /// [DataMember(Name="collection"), Preserve] public string Collection { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="key"), Preserve] public string Key { get; set; } /// [DataMember(Name="permission_read"), Preserve] public int PermissionRead { get; set; } /// [DataMember(Name="permission_write"), Preserve] public int PermissionWrite { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } /// [DataMember(Name="version"), Preserve] public string Version { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Collection: ", Collection, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Key: ", Key, ", "); output = string.Concat(output, "PermissionRead: ", PermissionRead, ", "); output = string.Concat(output, "PermissionWrite: ", PermissionWrite, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); output = string.Concat(output, "Value: ", Value, ", "); output = string.Concat(output, "Version: ", Version, ", "); return output; } } /// /// A storage acknowledgement. /// public interface IApiStorageObjectAck { /// /// The collection which stores the object. /// string Collection { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was created. /// string CreateTime { get; } /// /// The key of the object within the collection. /// string Key { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was last updated. /// string UpdateTime { get; } /// /// The owner of the object. /// string UserId { get; } /// /// The version hash of the object. /// string Version { get; } } /// internal class ApiStorageObjectAck : IApiStorageObjectAck { /// [DataMember(Name="collection"), Preserve] public string Collection { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="key"), Preserve] public string Key { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } /// [DataMember(Name="version"), Preserve] public string Version { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Collection: ", Collection, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Key: ", Key, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); output = string.Concat(output, "Version: ", Version, ", "); return output; } } /// /// Batch of acknowledgements for the storage object write. /// public interface IApiStorageObjectAcks { /// /// Batch of storage write acknowledgements. /// IEnumerable Acks { get; } } /// internal class ApiStorageObjectAcks : IApiStorageObjectAcks { /// [IgnoreDataMember] public IEnumerable Acks => _acks ?? new List(0); [DataMember(Name="acks"), Preserve] public List _acks { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Acks: [", string.Join(", ", Acks), "], "); return output; } } /// /// List of storage objects. /// public interface IApiStorageObjectList { /// /// The cursor for the next page of results, if any. /// string Cursor { get; } /// /// The list of storage objects. /// IEnumerable Objects { get; } } /// internal class ApiStorageObjectList : IApiStorageObjectList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable Objects => _objects ?? new List(0); [DataMember(Name="objects"), Preserve] public List _objects { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "Objects: [", string.Join(", ", Objects), "], "); return output; } } /// /// Batch of storage objects. /// public interface IApiStorageObjects { /// /// The batch of storage objects. /// IEnumerable Objects { get; } } /// internal class ApiStorageObjects : IApiStorageObjects { /// [IgnoreDataMember] public IEnumerable Objects => _objects ?? new List(0); [DataMember(Name="objects"), Preserve] public List _objects { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Objects: [", string.Join(", ", Objects), "], "); return output; } } /// /// Environment where a purchase/subscription took place, /// public enum ApiStoreEnvironment { /// /// - UNKNOWN: Unknown environment. /// UNKNOWN = 0, /// /// - SANDBOX: Sandbox/test environment. /// SANDBOX = 1, /// /// - PRODUCTION: Production environment. /// PRODUCTION = 2, } /// /// Validation Provider, /// public enum ApiStoreProvider { /// /// - APPLE_APP_STORE: Apple App Store /// APPLE_APP_STORE = 0, /// /// - GOOGLE_PLAY_STORE: Google Play Store /// GOOGLE_PLAY_STORE = 1, /// /// - HUAWEI_APP_GALLERY: Huawei App Gallery /// HUAWEI_APP_GALLERY = 2, /// /// - FACEBOOK_INSTANT_STORE: Facebook Instant Store /// FACEBOOK_INSTANT_STORE = 3, } /// /// A list of validated subscriptions stored by Nakama. /// public interface IApiSubscriptionList { /// /// The cursor to send when retrieving the next page, if any. /// string Cursor { get; } /// /// The cursor to send when retrieving the previous page, if any. /// string PrevCursor { get; } /// /// Stored validated subscriptions. /// IEnumerable ValidatedSubscriptions { get; } } /// internal class ApiSubscriptionList : IApiSubscriptionList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } /// [IgnoreDataMember] public IEnumerable ValidatedSubscriptions => _validatedSubscriptions ?? new List(0); [DataMember(Name="validated_subscriptions"), Preserve] public List _validatedSubscriptions { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); output = string.Concat(output, "ValidatedSubscriptions: [", string.Join(", ", ValidatedSubscriptions), "], "); return output; } } /// /// A tournament on the server. /// public interface IApiTournament { /// /// Whether the leaderboard was created authoritatively or not. /// bool Authoritative { get; } /// /// True if the tournament is active and can enter. A computed value. /// bool CanEnter { get; } /// /// The category of the tournament. e.g. "vip" could be category 1. /// int Category { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the tournament was created. /// string CreateTime { get; } /// /// The description of the tournament. May be blank. /// string Description { get; } /// /// Duration of the tournament in seconds. /// int Duration { get; } /// /// The UNIX time when the tournament stops being active until next reset. A computed value. /// int EndActive { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the tournament will be stopped. /// string EndTime { get; } /// /// The ID of the tournament. /// string Id { get; } /// /// Whether the user must join the tournament before being able to submit scores. /// bool JoinRequired { get; } /// /// The maximum score updates allowed per player for the current tournament. /// int MaxNumScore { get; } /// /// The maximum number of players for the tournament. /// int MaxSize { get; } /// /// Additional information stored as a JSON object. /// string Metadata { get; } /// /// The UNIX time when the tournament is next playable. A computed value. /// int NextReset { get; } /// /// Operator. /// ApiOperator Operator { get; } /// /// The UNIX time when the tournament was last reset. A computed value. /// int PrevReset { get; } /// /// The current number of players in the tournament. /// int Size { get; } /// /// ASC (0) or DESC (1) sort mode of scores in the tournament. /// int SortOrder { get; } /// /// The UNIX time when the tournament start being active. A computed value. /// int StartActive { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the tournament will start. /// string StartTime { get; } /// /// The title for the tournament. /// string Title { get; } } /// internal class ApiTournament : IApiTournament { /// [DataMember(Name="authoritative"), Preserve] public bool Authoritative { get; set; } /// [DataMember(Name="can_enter"), Preserve] public bool CanEnter { get; set; } /// [DataMember(Name="category"), Preserve] public int Category { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="description"), Preserve] public string Description { get; set; } /// [DataMember(Name="duration"), Preserve] public int Duration { get; set; } /// [DataMember(Name="end_active"), Preserve] public int EndActive { get; set; } /// [DataMember(Name="end_time"), Preserve] public string EndTime { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="join_required"), Preserve] public bool JoinRequired { get; set; } /// [DataMember(Name="max_num_score"), Preserve] public int MaxNumScore { get; set; } /// [DataMember(Name="max_size"), Preserve] public int MaxSize { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="next_reset"), Preserve] public int NextReset { get; set; } /// [IgnoreDataMember] public ApiOperator Operator => _operator; [DataMember(Name="operator"), Preserve] public ApiOperator _operator { get; set; } /// [DataMember(Name="prev_reset"), Preserve] public int PrevReset { get; set; } /// [DataMember(Name="size"), Preserve] public int Size { get; set; } /// [DataMember(Name="sort_order"), Preserve] public int SortOrder { get; set; } /// [DataMember(Name="start_active"), Preserve] public int StartActive { get; set; } /// [DataMember(Name="start_time"), Preserve] public string StartTime { get; set; } /// [DataMember(Name="title"), Preserve] public string Title { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Authoritative: ", Authoritative, ", "); output = string.Concat(output, "CanEnter: ", CanEnter, ", "); output = string.Concat(output, "Category: ", Category, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Description: ", Description, ", "); output = string.Concat(output, "Duration: ", Duration, ", "); output = string.Concat(output, "EndActive: ", EndActive, ", "); output = string.Concat(output, "EndTime: ", EndTime, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "JoinRequired: ", JoinRequired, ", "); output = string.Concat(output, "MaxNumScore: ", MaxNumScore, ", "); output = string.Concat(output, "MaxSize: ", MaxSize, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "NextReset: ", NextReset, ", "); output = string.Concat(output, "Operator: ", Operator, ", "); output = string.Concat(output, "PrevReset: ", PrevReset, ", "); output = string.Concat(output, "Size: ", Size, ", "); output = string.Concat(output, "SortOrder: ", SortOrder, ", "); output = string.Concat(output, "StartActive: ", StartActive, ", "); output = string.Concat(output, "StartTime: ", StartTime, ", "); output = string.Concat(output, "Title: ", Title, ", "); return output; } } /// /// A list of tournaments. /// public interface IApiTournamentList { /// /// A pagination cursor (optional). /// string Cursor { get; } /// /// The list of tournaments returned. /// IEnumerable Tournaments { get; } } /// internal class ApiTournamentList : IApiTournamentList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable Tournaments => _tournaments ?? new List(0); [DataMember(Name="tournaments"), Preserve] public List _tournaments { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "Tournaments: [", string.Join(", ", Tournaments), "], "); return output; } } /// /// A set of tournament records which may be part of a tournament records page or a batch of individual records. /// public interface IApiTournamentRecordList { /// /// The cursor to send when retireving the next page (optional). /// string NextCursor { get; } /// /// A batched set of tournament records belonging to specified owners. /// IEnumerable OwnerRecords { get; } /// /// The cursor to send when retrieving the previous page (optional). /// string PrevCursor { get; } /// /// The total number of ranks available. /// string RankCount { get; } /// /// A list of tournament records. /// IEnumerable Records { get; } } /// internal class ApiTournamentRecordList : IApiTournamentRecordList { /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [IgnoreDataMember] public IEnumerable OwnerRecords => _ownerRecords ?? new List(0); [DataMember(Name="owner_records"), Preserve] public List _ownerRecords { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } /// [DataMember(Name="rank_count"), Preserve] public string RankCount { get; set; } /// [IgnoreDataMember] public IEnumerable Records => _records ?? new List(0); [DataMember(Name="records"), Preserve] public List _records { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "OwnerRecords: [", string.Join(", ", OwnerRecords), "], "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); output = string.Concat(output, "RankCount: ", RankCount, ", "); output = string.Concat(output, "Records: [", string.Join(", ", Records), "], "); return output; } } /// /// Update a user's account details. /// public interface IApiUpdateAccountRequest { /// /// A URL for an avatar image. /// string AvatarUrl { get; } /// /// The display name of the user. /// string DisplayName { get; } /// /// The language expected to be a tag which follows the BCP-47 spec. /// string LangTag { get; } /// /// The location set by the user. /// string Location { get; } /// /// The timezone set by the user. /// string Timezone { get; } /// /// The username of the user's account. /// string Username { get; } } /// internal class ApiUpdateAccountRequest : IApiUpdateAccountRequest { /// [DataMember(Name="avatar_url"), Preserve] public string AvatarUrl { get; set; } /// [DataMember(Name="display_name"), Preserve] public string DisplayName { get; set; } /// [DataMember(Name="lang_tag"), Preserve] public string LangTag { get; set; } /// [DataMember(Name="location"), Preserve] public string Location { get; set; } /// [DataMember(Name="timezone"), Preserve] public string Timezone { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AvatarUrl: ", AvatarUrl, ", "); output = string.Concat(output, "DisplayName: ", DisplayName, ", "); output = string.Concat(output, "LangTag: ", LangTag, ", "); output = string.Concat(output, "Location: ", Location, ", "); output = string.Concat(output, "Timezone: ", Timezone, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// A user in the server. /// public interface IApiUser { /// /// The Apple Sign In ID in the user's account. /// string AppleId { get; } /// /// A URL for an avatar image. /// string AvatarUrl { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user was created. /// string CreateTime { get; } /// /// The display name of the user. /// string DisplayName { get; } /// /// Number of related edges to this user. /// int EdgeCount { get; } /// /// The Facebook id in the user's account. /// string FacebookId { get; } /// /// The Facebook Instant Game ID in the user's account. /// string FacebookInstantGameId { get; } /// /// The Apple Game Center in of the user's account. /// string GamecenterId { get; } /// /// The Google id in the user's account. /// string GoogleId { get; } /// /// The id of the user's account. /// string Id { get; } /// /// The language expected to be a tag which follows the BCP-47 spec. /// string LangTag { get; } /// /// The location set by the user. /// string Location { get; } /// /// Additional information stored as a JSON object. /// string Metadata { get; } /// /// Indicates whether the user is currently online. /// bool Online { get; } /// /// The Steam id in the user's account. /// string SteamId { get; } /// /// The timezone set by the user. /// string Timezone { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user was last updated. /// string UpdateTime { get; } /// /// The username of the user's account. /// string Username { get; } } /// internal class ApiUser : IApiUser { /// [DataMember(Name="apple_id"), Preserve] public string AppleId { get; set; } /// [DataMember(Name="avatar_url"), Preserve] public string AvatarUrl { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="display_name"), Preserve] public string DisplayName { get; set; } /// [DataMember(Name="edge_count"), Preserve] public int EdgeCount { get; set; } /// [DataMember(Name="facebook_id"), Preserve] public string FacebookId { get; set; } /// [DataMember(Name="facebook_instant_game_id"), Preserve] public string FacebookInstantGameId { get; set; } /// [DataMember(Name="gamecenter_id"), Preserve] public string GamecenterId { get; set; } /// [DataMember(Name="google_id"), Preserve] public string GoogleId { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="lang_tag"), Preserve] public string LangTag { get; set; } /// [DataMember(Name="location"), Preserve] public string Location { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="online"), Preserve] public bool Online { get; set; } /// [DataMember(Name="steam_id"), Preserve] public string SteamId { get; set; } /// [DataMember(Name="timezone"), Preserve] public string Timezone { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AppleId: ", AppleId, ", "); output = string.Concat(output, "AvatarUrl: ", AvatarUrl, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "DisplayName: ", DisplayName, ", "); output = string.Concat(output, "EdgeCount: ", EdgeCount, ", "); output = string.Concat(output, "FacebookId: ", FacebookId, ", "); output = string.Concat(output, "FacebookInstantGameId: ", FacebookInstantGameId, ", "); output = string.Concat(output, "GamecenterId: ", GamecenterId, ", "); output = string.Concat(output, "GoogleId: ", GoogleId, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "LangTag: ", LangTag, ", "); output = string.Concat(output, "Location: ", Location, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "Online: ", Online, ", "); output = string.Concat(output, "SteamId: ", SteamId, ", "); output = string.Concat(output, "Timezone: ", Timezone, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// A list of groups belonging to a user, along with the user's role in each group. /// public interface IApiUserGroupList { /// /// Cursor for the next page of results, if any. /// string Cursor { get; } /// /// Group-role pairs for a user. /// IEnumerable UserGroups { get; } } /// internal class ApiUserGroupList : IApiUserGroupList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable UserGroups => _userGroups ?? new List(0); [DataMember(Name="user_groups"), Preserve] public List _userGroups { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "UserGroups: [", string.Join(", ", UserGroups), "], "); return output; } } /// /// A collection of zero or more users. /// public interface IApiUsers { /// /// The User objects. /// IEnumerable Users { get; } } /// internal class ApiUsers : IApiUsers { /// [IgnoreDataMember] public IEnumerable Users => _users ?? new List(0); [DataMember(Name="users"), Preserve] public List _users { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Users: [", string.Join(", ", Users), "], "); return output; } } /// /// Apple IAP Purchases validation request /// public interface IApiValidatePurchaseAppleRequest { /// /// Persist the purchase /// bool Persist { get; } /// /// Base64 encoded Apple receipt data payload. /// string Receipt { get; } } /// internal class ApiValidatePurchaseAppleRequest : IApiValidatePurchaseAppleRequest { /// [DataMember(Name="persist"), Preserve] public bool Persist { get; set; } /// [DataMember(Name="receipt"), Preserve] public string Receipt { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Persist: ", Persist, ", "); output = string.Concat(output, "Receipt: ", Receipt, ", "); return output; } } /// /// Facebook Instant IAP Purchase validation request /// public interface IApiValidatePurchaseFacebookInstantRequest { /// /// Persist the purchase /// bool Persist { get; } /// /// Base64 encoded Facebook Instant signedRequest receipt data payload. /// string SignedRequest { get; } } /// internal class ApiValidatePurchaseFacebookInstantRequest : IApiValidatePurchaseFacebookInstantRequest { /// [DataMember(Name="persist"), Preserve] public bool Persist { get; set; } /// [DataMember(Name="signed_request"), Preserve] public string SignedRequest { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Persist: ", Persist, ", "); output = string.Concat(output, "SignedRequest: ", SignedRequest, ", "); return output; } } /// /// Google IAP Purchase validation request /// public interface IApiValidatePurchaseGoogleRequest { /// /// Persist the purchase /// bool Persist { get; } /// /// JSON encoded Google purchase payload. /// string Purchase { get; } } /// internal class ApiValidatePurchaseGoogleRequest : IApiValidatePurchaseGoogleRequest { /// [DataMember(Name="persist"), Preserve] public bool Persist { get; set; } /// [DataMember(Name="purchase"), Preserve] public string Purchase { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Persist: ", Persist, ", "); output = string.Concat(output, "Purchase: ", Purchase, ", "); return output; } } /// /// Huawei IAP Purchase validation request /// public interface IApiValidatePurchaseHuaweiRequest { /// /// Persist the purchase /// bool Persist { get; } /// /// JSON encoded Huawei InAppPurchaseData. /// string Purchase { get; } /// /// InAppPurchaseData signature. /// string Signature { get; } } /// internal class ApiValidatePurchaseHuaweiRequest : IApiValidatePurchaseHuaweiRequest { /// [DataMember(Name="persist"), Preserve] public bool Persist { get; set; } /// [DataMember(Name="purchase"), Preserve] public string Purchase { get; set; } /// [DataMember(Name="signature"), Preserve] public string Signature { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Persist: ", Persist, ", "); output = string.Concat(output, "Purchase: ", Purchase, ", "); output = string.Concat(output, "Signature: ", Signature, ", "); return output; } } /// /// Validate IAP response. /// public interface IApiValidatePurchaseResponse { /// /// Newly seen validated purchases. /// IEnumerable ValidatedPurchases { get; } } /// internal class ApiValidatePurchaseResponse : IApiValidatePurchaseResponse { /// [IgnoreDataMember] public IEnumerable ValidatedPurchases => _validatedPurchases ?? new List(0); [DataMember(Name="validated_purchases"), Preserve] public List _validatedPurchases { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ValidatedPurchases: [", string.Join(", ", ValidatedPurchases), "], "); return output; } } /// /// Apple Subscription validation request /// public interface IApiValidateSubscriptionAppleRequest { /// /// Persist the subscription. /// bool Persist { get; } /// /// Base64 encoded Apple receipt data payload. /// string Receipt { get; } } /// internal class ApiValidateSubscriptionAppleRequest : IApiValidateSubscriptionAppleRequest { /// [DataMember(Name="persist"), Preserve] public bool Persist { get; set; } /// [DataMember(Name="receipt"), Preserve] public string Receipt { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Persist: ", Persist, ", "); output = string.Concat(output, "Receipt: ", Receipt, ", "); return output; } } /// /// Google Subscription validation request /// public interface IApiValidateSubscriptionGoogleRequest { /// /// Persist the subscription. /// bool Persist { get; } /// /// JSON encoded Google purchase payload. /// string Receipt { get; } } /// internal class ApiValidateSubscriptionGoogleRequest : IApiValidateSubscriptionGoogleRequest { /// [DataMember(Name="persist"), Preserve] public bool Persist { get; set; } /// [DataMember(Name="receipt"), Preserve] public string Receipt { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Persist: ", Persist, ", "); output = string.Concat(output, "Receipt: ", Receipt, ", "); return output; } } /// /// Validate Subscription response. /// public interface IApiValidateSubscriptionResponse { /// /// /// IApiValidatedSubscription ValidatedSubscription { get; } } /// internal class ApiValidateSubscriptionResponse : IApiValidateSubscriptionResponse { /// [IgnoreDataMember] public IApiValidatedSubscription ValidatedSubscription => _validatedSubscription; [DataMember(Name="validated_subscription"), Preserve] public ApiValidatedSubscription _validatedSubscription { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ValidatedSubscription: ", ValidatedSubscription, ", "); return output; } } /// /// Validated Purchase stored by Nakama. /// public interface IApiValidatedPurchase { /// /// Timestamp when the receipt validation was stored in DB. /// string CreateTime { get; } /// /// Whether the purchase was done in production or sandbox environment. /// ApiStoreEnvironment Environment { get; } /// /// Purchase Product ID. /// string ProductId { get; } /// /// Raw provider validation response. /// string ProviderResponse { get; } /// /// Timestamp when the purchase was done. /// string PurchaseTime { get; } /// /// Timestamp when the purchase was refunded. Set to UNIX /// string RefundTime { get; } /// /// Whether the purchase had already been validated by Nakama before. /// bool SeenBefore { get; } /// /// Store identifier /// ApiStoreProvider Store { get; } /// /// Purchase Transaction ID. /// string TransactionId { get; } /// /// Timestamp when the receipt validation was updated in DB. /// string UpdateTime { get; } /// /// Purchase User ID. /// string UserId { get; } } /// internal class ApiValidatedPurchase : IApiValidatedPurchase { /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [IgnoreDataMember] public ApiStoreEnvironment Environment => _environment; [DataMember(Name="environment"), Preserve] public ApiStoreEnvironment _environment { get; set; } /// [DataMember(Name="product_id"), Preserve] public string ProductId { get; set; } /// [DataMember(Name="provider_response"), Preserve] public string ProviderResponse { get; set; } /// [DataMember(Name="purchase_time"), Preserve] public string PurchaseTime { get; set; } /// [DataMember(Name="refund_time"), Preserve] public string RefundTime { get; set; } /// [DataMember(Name="seen_before"), Preserve] public bool SeenBefore { get; set; } /// [IgnoreDataMember] public ApiStoreProvider Store => _store; [DataMember(Name="store"), Preserve] public ApiStoreProvider _store { get; set; } /// [DataMember(Name="transaction_id"), Preserve] public string TransactionId { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Environment: ", Environment, ", "); output = string.Concat(output, "ProductId: ", ProductId, ", "); output = string.Concat(output, "ProviderResponse: ", ProviderResponse, ", "); output = string.Concat(output, "PurchaseTime: ", PurchaseTime, ", "); output = string.Concat(output, "RefundTime: ", RefundTime, ", "); output = string.Concat(output, "SeenBefore: ", SeenBefore, ", "); output = string.Concat(output, "Store: ", Store, ", "); output = string.Concat(output, "TransactionId: ", TransactionId, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); return output; } } /// /// /// public interface IApiValidatedSubscription { /// /// Whether the subscription is currently active or not. /// bool Active { get; } /// /// UNIX Timestamp when the receipt validation was stored in DB. /// string CreateTime { get; } /// /// Whether the purchase was done in production or sandbox environment. /// ApiStoreEnvironment Environment { get; } /// /// Subscription expiration time. The subscription can still be auto-renewed to extend the expiration time further. /// string ExpiryTime { get; } /// /// Purchase Original transaction ID (we only keep track of the original subscription, not subsequent renewals). /// string OriginalTransactionId { get; } /// /// Purchase Product ID. /// string ProductId { get; } /// /// Raw provider notification body. /// string ProviderNotification { get; } /// /// Raw provider validation response body. /// string ProviderResponse { get; } /// /// UNIX Timestamp when the purchase was done. /// string PurchaseTime { get; } /// /// Subscription refund time. If this time is set, the subscription was refunded. /// string RefundTime { get; } /// /// Store identifier /// ApiStoreProvider Store { get; } /// /// UNIX Timestamp when the receipt validation was updated in DB. /// string UpdateTime { get; } /// /// Subscription User ID. /// string UserId { get; } } /// internal class ApiValidatedSubscription : IApiValidatedSubscription { /// [DataMember(Name="active"), Preserve] public bool Active { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [IgnoreDataMember] public ApiStoreEnvironment Environment => _environment; [DataMember(Name="environment"), Preserve] public ApiStoreEnvironment _environment { get; set; } /// [DataMember(Name="expiry_time"), Preserve] public string ExpiryTime { get; set; } /// [DataMember(Name="original_transaction_id"), Preserve] public string OriginalTransactionId { get; set; } /// [DataMember(Name="product_id"), Preserve] public string ProductId { get; set; } /// [DataMember(Name="provider_notification"), Preserve] public string ProviderNotification { get; set; } /// [DataMember(Name="provider_response"), Preserve] public string ProviderResponse { get; set; } /// [DataMember(Name="purchase_time"), Preserve] public string PurchaseTime { get; set; } /// [DataMember(Name="refund_time"), Preserve] public string RefundTime { get; set; } /// [IgnoreDataMember] public ApiStoreProvider Store => _store; [DataMember(Name="store"), Preserve] public ApiStoreProvider _store { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Active: ", Active, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Environment: ", Environment, ", "); output = string.Concat(output, "ExpiryTime: ", ExpiryTime, ", "); output = string.Concat(output, "OriginalTransactionId: ", OriginalTransactionId, ", "); output = string.Concat(output, "ProductId: ", ProductId, ", "); output = string.Concat(output, "ProviderNotification: ", ProviderNotification, ", "); output = string.Concat(output, "ProviderResponse: ", ProviderResponse, ", "); output = string.Concat(output, "PurchaseTime: ", PurchaseTime, ", "); output = string.Concat(output, "RefundTime: ", RefundTime, ", "); output = string.Concat(output, "Store: ", Store, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); return output; } } /// /// The object to store. /// public interface IApiWriteStorageObject { /// /// The collection to store the object. /// string Collection { get; } /// /// The key for the object within the collection. /// string Key { get; } /// /// The read access permissions for the object. /// int PermissionRead { get; } /// /// The write access permissions for the object. /// int PermissionWrite { get; } /// /// The value of the object. /// string Value { get; } /// /// The version hash of the object to check. Possible values are: ["", "*", "#hash#"]. if-match and if-none-match /// string Version { get; } } /// internal class ApiWriteStorageObject : IApiWriteStorageObject { /// [DataMember(Name="collection"), Preserve] public string Collection { get; set; } /// [DataMember(Name="key"), Preserve] public string Key { get; set; } /// [DataMember(Name="permission_read"), Preserve] public int PermissionRead { get; set; } /// [DataMember(Name="permission_write"), Preserve] public int PermissionWrite { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } /// [DataMember(Name="version"), Preserve] public string Version { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Collection: ", Collection, ", "); output = string.Concat(output, "Key: ", Key, ", "); output = string.Concat(output, "PermissionRead: ", PermissionRead, ", "); output = string.Concat(output, "PermissionWrite: ", PermissionWrite, ", "); output = string.Concat(output, "Value: ", Value, ", "); output = string.Concat(output, "Version: ", Version, ", "); return output; } } /// /// Write objects to the storage engine. /// public interface IApiWriteStorageObjectsRequest { /// /// The objects to store on the server. /// IEnumerable Objects { get; } } /// internal class ApiWriteStorageObjectsRequest : IApiWriteStorageObjectsRequest { /// [IgnoreDataMember] public IEnumerable Objects => _objects ?? new List(0); [DataMember(Name="objects"), Preserve] public List _objects { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Objects: [", string.Join(", ", Objects), "], "); return output; } } /// /// /// public interface IProtobufAny { /// /// /// string @type { get; } } /// internal class ProtobufAny : IProtobufAny { /// [DataMember(Name="@type"), Preserve] public string @type { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "@type: ", @type, ", "); return output; } } /// /// /// public interface IRpcStatus { /// /// /// int Code { get; } /// /// /// IEnumerable Details { get; } /// /// /// string Message { get; } } /// internal class RpcStatus : IRpcStatus { /// [DataMember(Name="code"), Preserve] public int Code { get; set; } /// [IgnoreDataMember] public IEnumerable Details => _details ?? new List(0); [DataMember(Name="details"), Preserve] public List _details { get; set; } /// [DataMember(Name="message"), Preserve] public string Message { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Code: ", Code, ", "); output = string.Concat(output, "Details: [", string.Join(", ", Details), "], "); output = string.Concat(output, "Message: ", Message, ", "); return output; } } /// /// The low level client for the Nakama API. /// internal class ApiClient { public readonly IHttpAdapter HttpAdapter; public int Timeout { get; set; } private readonly Uri _baseUri; public ApiClient(Uri baseUri, IHttpAdapter httpAdapter, int timeout = 10) { _baseUri = baseUri; HttpAdapter = httpAdapter; Timeout = timeout; } /// /// A healthcheck which load balancers can use to check the service. /// public async Task HealthcheckAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/healthcheck"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Delete the current user's account. /// public async Task DeleteAccountAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/account"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Fetch the current user's account. /// public async Task GetAccountAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/account"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Update fields in the current user's account. /// public async Task UpdateAccountAsync( string bearerToken, ApiUpdateAccountRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "PUT"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Authenticate a user with an Apple ID against the server. /// public async Task AuthenticateAppleAsync( string basicAuthUsername, string basicAuthPassword, ApiAccountApple account, bool? create, string username, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/authenticate/apple"; var queryParams = ""; if (create != null) { queryParams = string.Concat(queryParams, "create=", create.ToString().ToLower(), "&"); } if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Authenticate a user with a custom id against the server. /// public async Task AuthenticateCustomAsync( string basicAuthUsername, string basicAuthPassword, ApiAccountCustom account, bool? create, string username, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/authenticate/custom"; var queryParams = ""; if (create != null) { queryParams = string.Concat(queryParams, "create=", create.ToString().ToLower(), "&"); } if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Authenticate a user with a device id against the server. /// public async Task AuthenticateDeviceAsync( string basicAuthUsername, string basicAuthPassword, ApiAccountDevice account, bool? create, string username, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/authenticate/device"; var queryParams = ""; if (create != null) { queryParams = string.Concat(queryParams, "create=", create.ToString().ToLower(), "&"); } if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Authenticate a user with an email+password against the server. /// public async Task AuthenticateEmailAsync( string basicAuthUsername, string basicAuthPassword, ApiAccountEmail account, bool? create, string username, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/authenticate/email"; var queryParams = ""; if (create != null) { queryParams = string.Concat(queryParams, "create=", create.ToString().ToLower(), "&"); } if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Authenticate a user with a Facebook OAuth token against the server. /// public async Task AuthenticateFacebookAsync( string basicAuthUsername, string basicAuthPassword, ApiAccountFacebook account, bool? create, string username, bool? sync, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/authenticate/facebook"; var queryParams = ""; if (create != null) { queryParams = string.Concat(queryParams, "create=", create.ToString().ToLower(), "&"); } if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } if (sync != null) { queryParams = string.Concat(queryParams, "sync=", sync.ToString().ToLower(), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Authenticate a user with a Facebook Instant Game token against the server. /// public async Task AuthenticateFacebookInstantGameAsync( string basicAuthUsername, string basicAuthPassword, ApiAccountFacebookInstantGame account, bool? create, string username, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/authenticate/facebookinstantgame"; var queryParams = ""; if (create != null) { queryParams = string.Concat(queryParams, "create=", create.ToString().ToLower(), "&"); } if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Authenticate a user with Apple's GameCenter against the server. /// public async Task AuthenticateGameCenterAsync( string basicAuthUsername, string basicAuthPassword, ApiAccountGameCenter account, bool? create, string username, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/authenticate/gamecenter"; var queryParams = ""; if (create != null) { queryParams = string.Concat(queryParams, "create=", create.ToString().ToLower(), "&"); } if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Authenticate a user with Google against the server. /// public async Task AuthenticateGoogleAsync( string basicAuthUsername, string basicAuthPassword, ApiAccountGoogle account, bool? create, string username, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/authenticate/google"; var queryParams = ""; if (create != null) { queryParams = string.Concat(queryParams, "create=", create.ToString().ToLower(), "&"); } if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Authenticate a user with Steam against the server. /// public async Task AuthenticateSteamAsync( string basicAuthUsername, string basicAuthPassword, ApiAccountSteam account, bool? create, string username, bool? sync, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/authenticate/steam"; var queryParams = ""; if (create != null) { queryParams = string.Concat(queryParams, "create=", create.ToString().ToLower(), "&"); } if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } if (sync != null) { queryParams = string.Concat(queryParams, "sync=", sync.ToString().ToLower(), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Add an Apple ID to the social profiles on the current user's account. /// public async Task LinkAppleAsync( string bearerToken, ApiAccountApple body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/link/apple"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Add a custom ID to the social profiles on the current user's account. /// public async Task LinkCustomAsync( string bearerToken, ApiAccountCustom body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/link/custom"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Add a device ID to the social profiles on the current user's account. /// public async Task LinkDeviceAsync( string bearerToken, ApiAccountDevice body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/link/device"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Add an email+password to the social profiles on the current user's account. /// public async Task LinkEmailAsync( string bearerToken, ApiAccountEmail body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/link/email"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Add Facebook to the social profiles on the current user's account. /// public async Task LinkFacebookAsync( string bearerToken, ApiAccountFacebook account, bool? sync, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/account/link/facebook"; var queryParams = ""; if (sync != null) { queryParams = string.Concat(queryParams, "sync=", sync.ToString().ToLower(), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Add Facebook Instant Game to the social profiles on the current user's account. /// public async Task LinkFacebookInstantGameAsync( string bearerToken, ApiAccountFacebookInstantGame body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/link/facebookinstantgame"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Add Apple's GameCenter to the social profiles on the current user's account. /// public async Task LinkGameCenterAsync( string bearerToken, ApiAccountGameCenter body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/link/gamecenter"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Add Google to the social profiles on the current user's account. /// public async Task LinkGoogleAsync( string bearerToken, ApiAccountGoogle body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/link/google"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Add Steam to the social profiles on the current user's account. /// public async Task LinkSteamAsync( string bearerToken, ApiLinkSteamRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/link/steam"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Refresh a user's session using a refresh token retrieved from a previous authentication request. /// public async Task SessionRefreshAsync( string basicAuthUsername, string basicAuthPassword, ApiSessionRefreshRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/session/refresh"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Remove the Apple ID from the social profiles on the current user's account. /// public async Task UnlinkAppleAsync( string bearerToken, ApiAccountApple body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/unlink/apple"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Remove the custom ID from the social profiles on the current user's account. /// public async Task UnlinkCustomAsync( string bearerToken, ApiAccountCustom body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/unlink/custom"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Remove the device ID from the social profiles on the current user's account. /// public async Task UnlinkDeviceAsync( string bearerToken, ApiAccountDevice body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/unlink/device"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Remove the email+password from the social profiles on the current user's account. /// public async Task UnlinkEmailAsync( string bearerToken, ApiAccountEmail body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/unlink/email"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Remove Facebook from the social profiles on the current user's account. /// public async Task UnlinkFacebookAsync( string bearerToken, ApiAccountFacebook body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/unlink/facebook"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Remove Facebook Instant Game profile from the social profiles on the current user's account. /// public async Task UnlinkFacebookInstantGameAsync( string bearerToken, ApiAccountFacebookInstantGame body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/unlink/facebookinstantgame"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Remove Apple's GameCenter from the social profiles on the current user's account. /// public async Task UnlinkGameCenterAsync( string bearerToken, ApiAccountGameCenter body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/unlink/gamecenter"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Remove Google from the social profiles on the current user's account. /// public async Task UnlinkGoogleAsync( string bearerToken, ApiAccountGoogle body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/unlink/google"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Remove Steam from the social profiles on the current user's account. /// public async Task UnlinkSteamAsync( string bearerToken, ApiAccountSteam body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/account/unlink/steam"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// List a channel's message history. /// public async Task ListChannelMessagesAsync( string bearerToken, string channelId, int? limit, bool? forward, string cursor, CancellationToken? cancellationToken) { if (channelId == null) { throw new ArgumentException("'channelId' is required but was null."); } var urlpath = "/v2/channel/{channelId}"; urlpath = urlpath.Replace("{channelId}", Uri.EscapeDataString(channelId)); var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (forward != null) { queryParams = string.Concat(queryParams, "forward=", forward.ToString().ToLower(), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Submit an event for processing in the server's registered runtime custom events handler. /// public async Task EventAsync( string bearerToken, ApiEvent body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/event"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Delete one or more users by ID or username. /// public async Task DeleteFriendsAsync( string bearerToken, IEnumerable ids, IEnumerable usernames, CancellationToken? cancellationToken) { var urlpath = "/v2/friend"; var queryParams = ""; foreach (var elem in ids ?? new string[0]) { queryParams = string.Concat(queryParams, "ids=", Uri.EscapeDataString(elem), "&"); } foreach (var elem in usernames ?? new string[0]) { queryParams = string.Concat(queryParams, "usernames=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// List all friends for the current user. /// public async Task ListFriendsAsync( string bearerToken, int? limit, int? state, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/friend"; var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (state != null) { queryParams = string.Concat(queryParams, "state=", state, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Add friends by ID or username to a user's account. /// public async Task AddFriendsAsync( string bearerToken, IEnumerable ids, IEnumerable usernames, string metadata, CancellationToken? cancellationToken) { var urlpath = "/v2/friend"; var queryParams = ""; foreach (var elem in ids ?? new string[0]) { queryParams = string.Concat(queryParams, "ids=", Uri.EscapeDataString(elem), "&"); } foreach (var elem in usernames ?? new string[0]) { queryParams = string.Concat(queryParams, "usernames=", Uri.EscapeDataString(elem), "&"); } if (metadata != null) { queryParams = string.Concat(queryParams, "metadata=", Uri.EscapeDataString(metadata), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Block one or more users by ID or username. /// public async Task BlockFriendsAsync( string bearerToken, IEnumerable ids, IEnumerable usernames, CancellationToken? cancellationToken) { var urlpath = "/v2/friend/block"; var queryParams = ""; foreach (var elem in ids ?? new string[0]) { queryParams = string.Concat(queryParams, "ids=", Uri.EscapeDataString(elem), "&"); } foreach (var elem in usernames ?? new string[0]) { queryParams = string.Concat(queryParams, "usernames=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Import Facebook friends and add them to a user's account. /// public async Task ImportFacebookFriendsAsync( string bearerToken, ApiAccountFacebook account, bool? reset, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/friend/facebook"; var queryParams = ""; if (reset != null) { queryParams = string.Concat(queryParams, "reset=", reset.ToString().ToLower(), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// List friends of friends for the current user. /// public async Task ListFriendsOfFriendsAsync( string bearerToken, int? limit, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/friend/friends"; var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Import Steam friends and add them to a user's account. /// public async Task ImportSteamFriendsAsync( string bearerToken, ApiAccountSteam account, bool? reset, CancellationToken? cancellationToken) { if (account == null) { throw new ArgumentException("'account' is required but was null."); } var urlpath = "/v2/friend/steam"; var queryParams = ""; if (reset != null) { queryParams = string.Concat(queryParams, "reset=", reset.ToString().ToLower(), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = account.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// List groups based on given filters. /// public async Task ListGroupsAsync( string bearerToken, string name, string cursor, int? limit, string langTag, int? members, bool? open, CancellationToken? cancellationToken) { var urlpath = "/v2/group"; var queryParams = ""; if (name != null) { queryParams = string.Concat(queryParams, "name=", Uri.EscapeDataString(name), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (langTag != null) { queryParams = string.Concat(queryParams, "lang_tag=", Uri.EscapeDataString(langTag), "&"); } if (members != null) { queryParams = string.Concat(queryParams, "members=", members, "&"); } if (open != null) { queryParams = string.Concat(queryParams, "open=", open.ToString().ToLower(), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Create a new group with the current user as the owner. /// public async Task CreateGroupAsync( string bearerToken, ApiCreateGroupRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/group"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete a group by ID. /// public async Task DeleteGroupAsync( string bearerToken, string groupId, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/group/{groupId}"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Update fields in a given group. /// public async Task UpdateGroupAsync( string bearerToken, string groupId, ApiUpdateGroupRequest body, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/group/{groupId}"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "PUT"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Add users to a group. /// public async Task AddGroupUsersAsync( string bearerToken, string groupId, IEnumerable userIds, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/group/{groupId}/add"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; foreach (var elem in userIds ?? new string[0]) { queryParams = string.Concat(queryParams, "user_ids=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Ban a set of users from a group. /// public async Task BanGroupUsersAsync( string bearerToken, string groupId, IEnumerable userIds, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/group/{groupId}/ban"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; foreach (var elem in userIds ?? new string[0]) { queryParams = string.Concat(queryParams, "user_ids=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Demote a set of users in a group to the next role down. /// public async Task DemoteGroupUsersAsync( string bearerToken, string groupId, IEnumerable userIds, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/group/{groupId}/demote"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; foreach (var elem in userIds ?? new string[0]) { queryParams = string.Concat(queryParams, "user_ids=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Immediately join an open group, or request to join a closed one. /// public async Task JoinGroupAsync( string bearerToken, string groupId, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/group/{groupId}/join"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Kick a set of users from a group. /// public async Task KickGroupUsersAsync( string bearerToken, string groupId, IEnumerable userIds, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/group/{groupId}/kick"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; foreach (var elem in userIds ?? new string[0]) { queryParams = string.Concat(queryParams, "user_ids=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Leave a group the user is a member of. /// public async Task LeaveGroupAsync( string bearerToken, string groupId, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/group/{groupId}/leave"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Promote a set of users in a group to the next role up. /// public async Task PromoteGroupUsersAsync( string bearerToken, string groupId, IEnumerable userIds, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/group/{groupId}/promote"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; foreach (var elem in userIds ?? new string[0]) { queryParams = string.Concat(queryParams, "user_ids=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// List all users that are part of a group. /// public async Task ListGroupUsersAsync( string bearerToken, string groupId, int? limit, int? state, string cursor, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/group/{groupId}/user"; urlpath = urlpath.Replace("{groupId}", Uri.EscapeDataString(groupId)); var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (state != null) { queryParams = string.Concat(queryParams, "state=", state, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Validate Apple IAP Receipt /// public async Task ValidatePurchaseAppleAsync( string bearerToken, ApiValidatePurchaseAppleRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/iap/purchase/apple"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Validate FB Instant IAP Receipt /// public async Task ValidatePurchaseFacebookInstantAsync( string bearerToken, ApiValidatePurchaseFacebookInstantRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/iap/purchase/facebookinstant"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Validate Google IAP Receipt /// public async Task ValidatePurchaseGoogleAsync( string bearerToken, ApiValidatePurchaseGoogleRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/iap/purchase/google"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Validate Huawei IAP Receipt /// public async Task ValidatePurchaseHuaweiAsync( string bearerToken, ApiValidatePurchaseHuaweiRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/iap/purchase/huawei"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List user's subscriptions. /// public async Task ListSubscriptionsAsync( string bearerToken, ApiListSubscriptionsRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/iap/subscription"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Validate Apple Subscription Receipt /// public async Task ValidateSubscriptionAppleAsync( string bearerToken, ApiValidateSubscriptionAppleRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/iap/subscription/apple"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Validate Google Subscription Receipt /// public async Task ValidateSubscriptionGoogleAsync( string bearerToken, ApiValidateSubscriptionGoogleRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/iap/subscription/google"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get subscription by product id. /// public async Task GetSubscriptionAsync( string bearerToken, string productId, CancellationToken? cancellationToken) { if (productId == null) { throw new ArgumentException("'productId' is required but was null."); } var urlpath = "/v2/iap/subscription/{productId}"; urlpath = urlpath.Replace("{productId}", Uri.EscapeDataString(productId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete a leaderboard record. /// public async Task DeleteLeaderboardRecordAsync( string bearerToken, string leaderboardId, CancellationToken? cancellationToken) { if (leaderboardId == null) { throw new ArgumentException("'leaderboardId' is required but was null."); } var urlpath = "/v2/leaderboard/{leaderboardId}"; urlpath = urlpath.Replace("{leaderboardId}", Uri.EscapeDataString(leaderboardId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// List leaderboard records. /// public async Task ListLeaderboardRecordsAsync( string bearerToken, string leaderboardId, IEnumerable ownerIds, int? limit, string cursor, string expiry, CancellationToken? cancellationToken) { if (leaderboardId == null) { throw new ArgumentException("'leaderboardId' is required but was null."); } var urlpath = "/v2/leaderboard/{leaderboardId}"; urlpath = urlpath.Replace("{leaderboardId}", Uri.EscapeDataString(leaderboardId)); var queryParams = ""; foreach (var elem in ownerIds ?? new string[0]) { queryParams = string.Concat(queryParams, "owner_ids=", Uri.EscapeDataString(elem), "&"); } if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } if (expiry != null) { queryParams = string.Concat(queryParams, "expiry=", Uri.EscapeDataString(expiry), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Write a record to a leaderboard. /// public async Task WriteLeaderboardRecordAsync( string bearerToken, string leaderboardId, WriteLeaderboardRecordRequestLeaderboardRecordWrite record, CancellationToken? cancellationToken) { if (leaderboardId == null) { throw new ArgumentException("'leaderboardId' is required but was null."); } if (record == null) { throw new ArgumentException("'record' is required but was null."); } var urlpath = "/v2/leaderboard/{leaderboardId}"; urlpath = urlpath.Replace("{leaderboardId}", Uri.EscapeDataString(leaderboardId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = record.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List leaderboard records around the target ownerId. /// public async Task ListLeaderboardRecordsAroundOwnerAsync( string bearerToken, string leaderboardId, string ownerId, int? limit, string expiry, string cursor, CancellationToken? cancellationToken) { if (leaderboardId == null) { throw new ArgumentException("'leaderboardId' is required but was null."); } if (ownerId == null) { throw new ArgumentException("'ownerId' is required but was null."); } var urlpath = "/v2/leaderboard/{leaderboardId}/owner/{ownerId}"; urlpath = urlpath.Replace("{leaderboardId}", Uri.EscapeDataString(leaderboardId)); urlpath = urlpath.Replace("{ownerId}", Uri.EscapeDataString(ownerId)); var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (expiry != null) { queryParams = string.Concat(queryParams, "expiry=", Uri.EscapeDataString(expiry), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List running matches and optionally filter by matching criteria. /// public async Task ListMatchesAsync( string bearerToken, int? limit, bool? authoritative, string label, int? minSize, int? maxSize, string query, CancellationToken? cancellationToken) { var urlpath = "/v2/match"; var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (authoritative != null) { queryParams = string.Concat(queryParams, "authoritative=", authoritative.ToString().ToLower(), "&"); } if (label != null) { queryParams = string.Concat(queryParams, "label=", Uri.EscapeDataString(label), "&"); } if (minSize != null) { queryParams = string.Concat(queryParams, "min_size=", minSize, "&"); } if (maxSize != null) { queryParams = string.Concat(queryParams, "max_size=", maxSize, "&"); } if (query != null) { queryParams = string.Concat(queryParams, "query=", Uri.EscapeDataString(query), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get matchmaker stats. /// public async Task GetMatchmakerStatsAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/matchmaker/stats"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete one or more notifications for the current user. /// public async Task DeleteNotificationsAsync( string bearerToken, IEnumerable ids, CancellationToken? cancellationToken) { var urlpath = "/v2/notification"; var queryParams = ""; foreach (var elem in ids ?? new string[0]) { queryParams = string.Concat(queryParams, "ids=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Fetch list of notifications. /// public async Task ListNotificationsAsync( string bearerToken, int? limit, string cacheableCursor, CancellationToken? cancellationToken) { var urlpath = "/v2/notification"; var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cacheableCursor != null) { queryParams = string.Concat(queryParams, "cacheable_cursor=", Uri.EscapeDataString(cacheableCursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List parties and optionally filter by matching criteria. /// public async Task ListPartiesAsync( string bearerToken, int? limit, bool? open, string query, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/party"; var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (open != null) { queryParams = string.Concat(queryParams, "open=", open.ToString().ToLower(), "&"); } if (query != null) { queryParams = string.Concat(queryParams, "query=", Uri.EscapeDataString(query), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Execute a Lua function on the server. /// public async Task RpcFunc2Async( string bearerToken, string basicAuthUsername, string basicAuthPassword, string id, string payload, string httpKey, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/rpc/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; if (payload != null) { queryParams = string.Concat(queryParams, "payload=", Uri.EscapeDataString(payload), "&"); } if (httpKey != null) { queryParams = string.Concat(queryParams, "http_key=", Uri.EscapeDataString(httpKey), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(bearerToken)) { var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); } if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Execute a Lua function on the server. /// public async Task RpcFuncAsync( string bearerToken, string basicAuthUsername, string basicAuthPassword, string id, string payload, string httpKey, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } if (payload == null) { throw new ArgumentException("'payload' is required but was null."); } var urlpath = "/v2/rpc/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; if (httpKey != null) { queryParams = string.Concat(queryParams, "http_key=", Uri.EscapeDataString(httpKey), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(bearerToken)) { var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); } if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = payload.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. /// public async Task SessionLogoutAsync( string bearerToken, ApiSessionLogoutRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/session/logout"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// Get storage objects. /// public async Task ReadStorageObjectsAsync( string bearerToken, ApiReadStorageObjectsRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/storage"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Write objects into the storage engine. /// public async Task WriteStorageObjectsAsync( string bearerToken, ApiWriteStorageObjectsRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/storage"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "PUT"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete one or more objects by ID or username. /// public async Task DeleteStorageObjectsAsync( string bearerToken, ApiDeleteStorageObjectsRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/storage/delete"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "PUT"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// List publicly readable storage objects in a given collection. /// public async Task ListStorageObjectsAsync( string bearerToken, string collection, string userId, int? limit, string cursor, CancellationToken? cancellationToken) { if (collection == null) { throw new ArgumentException("'collection' is required but was null."); } var urlpath = "/v2/storage/{collection}"; urlpath = urlpath.Replace("{collection}", Uri.EscapeDataString(collection)); var queryParams = ""; if (userId != null) { queryParams = string.Concat(queryParams, "user_id=", Uri.EscapeDataString(userId), "&"); } if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List publicly readable storage objects in a given collection. /// public async Task ListStorageObjects2Async( string bearerToken, string collection, string userId, int? limit, string cursor, CancellationToken? cancellationToken) { if (collection == null) { throw new ArgumentException("'collection' is required but was null."); } if (userId == null) { throw new ArgumentException("'userId' is required but was null."); } var urlpath = "/v2/storage/{collection}/{userId}"; urlpath = urlpath.Replace("{collection}", Uri.EscapeDataString(collection)); urlpath = urlpath.Replace("{userId}", Uri.EscapeDataString(userId)); var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List current or upcoming tournaments. /// public async Task ListTournamentsAsync( string bearerToken, int? categoryStart, int? categoryEnd, int? startTime, int? endTime, int? limit, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/tournament"; var queryParams = ""; if (categoryStart != null) { queryParams = string.Concat(queryParams, "category_start=", categoryStart, "&"); } if (categoryEnd != null) { queryParams = string.Concat(queryParams, "category_end=", categoryEnd, "&"); } if (startTime != null) { queryParams = string.Concat(queryParams, "start_time=", startTime, "&"); } if (endTime != null) { queryParams = string.Concat(queryParams, "end_time=", endTime, "&"); } if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete a tournament record. /// public async Task DeleteTournamentRecordAsync( string bearerToken, string tournamentId, CancellationToken? cancellationToken) { if (tournamentId == null) { throw new ArgumentException("'tournamentId' is required but was null."); } var urlpath = "/v2/tournament/{tournamentId}"; urlpath = urlpath.Replace("{tournamentId}", Uri.EscapeDataString(tournamentId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// List tournament records. /// public async Task ListTournamentRecordsAsync( string bearerToken, string tournamentId, IEnumerable ownerIds, int? limit, string cursor, string expiry, CancellationToken? cancellationToken) { if (tournamentId == null) { throw new ArgumentException("'tournamentId' is required but was null."); } var urlpath = "/v2/tournament/{tournamentId}"; urlpath = urlpath.Replace("{tournamentId}", Uri.EscapeDataString(tournamentId)); var queryParams = ""; foreach (var elem in ownerIds ?? new string[0]) { queryParams = string.Concat(queryParams, "owner_ids=", Uri.EscapeDataString(elem), "&"); } if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } if (expiry != null) { queryParams = string.Concat(queryParams, "expiry=", Uri.EscapeDataString(expiry), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Write a record to a tournament. /// public async Task WriteTournamentRecord2Async( string bearerToken, string tournamentId, WriteTournamentRecordRequestTournamentRecordWrite record, CancellationToken? cancellationToken) { if (tournamentId == null) { throw new ArgumentException("'tournamentId' is required but was null."); } if (record == null) { throw new ArgumentException("'record' is required but was null."); } var urlpath = "/v2/tournament/{tournamentId}"; urlpath = urlpath.Replace("{tournamentId}", Uri.EscapeDataString(tournamentId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = record.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Write a record to a tournament. /// public async Task WriteTournamentRecordAsync( string bearerToken, string tournamentId, WriteTournamentRecordRequestTournamentRecordWrite record, CancellationToken? cancellationToken) { if (tournamentId == null) { throw new ArgumentException("'tournamentId' is required but was null."); } if (record == null) { throw new ArgumentException("'record' is required but was null."); } var urlpath = "/v2/tournament/{tournamentId}"; urlpath = urlpath.Replace("{tournamentId}", Uri.EscapeDataString(tournamentId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "PUT"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = record.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Attempt to join an open and running tournament. /// public async Task JoinTournamentAsync( string bearerToken, string tournamentId, CancellationToken? cancellationToken) { if (tournamentId == null) { throw new ArgumentException("'tournamentId' is required but was null."); } var urlpath = "/v2/tournament/{tournamentId}/join"; urlpath = urlpath.Replace("{tournamentId}", Uri.EscapeDataString(tournamentId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); } /// /// List tournament records for a given owner. /// public async Task ListTournamentRecordsAroundOwnerAsync( string bearerToken, string tournamentId, string ownerId, int? limit, string expiry, string cursor, CancellationToken? cancellationToken) { if (tournamentId == null) { throw new ArgumentException("'tournamentId' is required but was null."); } if (ownerId == null) { throw new ArgumentException("'ownerId' is required but was null."); } var urlpath = "/v2/tournament/{tournamentId}/owner/{ownerId}"; urlpath = urlpath.Replace("{tournamentId}", Uri.EscapeDataString(tournamentId)); urlpath = urlpath.Replace("{ownerId}", Uri.EscapeDataString(ownerId)); var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (expiry != null) { queryParams = string.Concat(queryParams, "expiry=", Uri.EscapeDataString(expiry), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Fetch zero or more users by ID and/or username. /// public async Task GetUsersAsync( string bearerToken, IEnumerable ids, IEnumerable usernames, IEnumerable facebookIds, CancellationToken? cancellationToken) { var urlpath = "/v2/user"; var queryParams = ""; foreach (var elem in ids ?? new string[0]) { queryParams = string.Concat(queryParams, "ids=", Uri.EscapeDataString(elem), "&"); } foreach (var elem in usernames ?? new string[0]) { queryParams = string.Concat(queryParams, "usernames=", Uri.EscapeDataString(elem), "&"); } foreach (var elem in facebookIds ?? new string[0]) { queryParams = string.Concat(queryParams, "facebook_ids=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List groups the current user belongs to. /// public async Task ListUserGroupsAsync( string bearerToken, string userId, int? limit, int? state, string cursor, CancellationToken? cancellationToken) { if (userId == null) { throw new ArgumentException("'userId' is required but was null."); } var urlpath = "/v2/user/{userId}/group"; urlpath = urlpath.Replace("{userId}", Uri.EscapeDataString(userId)); var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (state != null) { queryParams = string.Concat(queryParams, "state=", state, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var method = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } } } ================================================ FILE: Nakama/ChannelJoinMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// Send a channel join message to the server. /// internal class ChannelJoinMessage { [DataMember(Name="hidden"), Preserve] public bool Hidden { get; set; } [DataMember(Name="persistence"), Preserve] public bool Persistence { get; set; } [DataMember(Name="target"), Preserve] public string Target { get; set; } [DataMember(Name="type"), Preserve] public int Type { get; set; } public override string ToString() { return $"ChannelJoinMessage(Hidden={Hidden}, Persistence={Persistence}, Target='{Target}', Type={Type})"; } } /// /// The available channel types on the server. /// public enum ChannelType : uint { /// /// A chat room which can be created dynamically with a name. /// Room = 1, /// /// A private chat between two users. /// DirectMessage = 2, /// /// A chat within a group on the server. /// Group = 3 } } ================================================ FILE: Nakama/ChannelLeaveMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// A leave message to a chat channel. /// internal class ChannelLeaveMessage { [DataMember(Name="channel_id"), Preserve] public string ChannelId { get; set; } public override string ToString() { return $"ChannelLeaveMessage(ChannelId='{ChannelId}')"; } } } ================================================ FILE: Nakama/ChannelRemoveMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// Remove a message from a chat channel. /// internal class ChannelRemoveMessage { [DataMember(Name="channel_id"), Preserve] public string ChannelId { get; set; } [DataMember(Name="message_id"), Preserve] public string MessageId { get; set; } public override string ToString() { return $"ChannelRemoveMessage(ChannelId='{ChannelId}', MessageId='{MessageId}')"; } } } ================================================ FILE: Nakama/ChannelSendMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// Send a chat message to a channel on the server. /// internal class ChannelSendMessage { [DataMember(Name="channel_id"), Preserve] public string ChannelId { get; set; } [DataMember(Name="content"), Preserve] public string Content { get; set; } public override string ToString() { return $"ChannelSendMessage(ChannelId='{ChannelId}', Content='{Content}')"; } } } ================================================ FILE: Nakama/ChannelUpdateMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// Update a chat message which has been sent to a channel. /// internal class ChannelUpdateMessage { [DataMember(Name="channel_id"), Preserve] public string ChannelId { get; set; } [DataMember(Name="message_id"), Preserve] public string MessageId { get; set; } [DataMember(Name="content"), Preserve] public string Content { get; set; } public override string ToString() { return $"ChannelUpdateMessage(ChannelId='{ChannelId}', MessageId='{MessageId}', Content='{Content}')"; } } } ================================================ FILE: Nakama/Client.cs ================================================ // Copyright 2022 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Nakama { /// public class Client : IClient { /// /// The default host address of the server. /// public const string DefaultHost = "127.0.0.1"; /// /// The default protocol scheme for the socket connection. /// public const string DefaultScheme = "http"; /// /// The default port number of the server. /// public const int DefaultPort = 7350; /// /// The default expired timespan used to check session lifetime. /// public static TimeSpan DefaultExpiredTimeSpan = TimeSpan.FromMinutes(5); /// public bool AutoRefreshSession { get; } /// public RetryConfiguration GlobalRetryConfiguration { get; set; } = new RetryConfiguration( baseDelayMs: 500, jitter: RetryJitter.FullJitter, listener: null, maxRetries: 4); /// public string Host { get; } /// /// The logger to use with the client. /// public ILogger Logger { get => _logger; set { _apiClient.HttpAdapter.Logger = value; _logger = value; } } /// public int Port { get; } /// public string Scheme { get; } /// public string ServerKey { get; } /// public event Action ReceivedSessionUpdated; /// public int Timeout { get => _apiClient.Timeout; set => _apiClient.Timeout = value; } private readonly ApiClient _apiClient; private ILogger _logger; private readonly RetryInvoker _retryInvoker; private const int DefaultTimeout = 15; /// There is a bug in Unity's WebGL implementation that prevents the proper invocation of constructors with more /// than four parameters. For this reason, avoid defining constructors that do this. public Client(string serverKey) : this(serverKey, HttpRequestAdapter.WithGzip()) { } public Client(string serverKey, IHttpAdapter adapter) : this(DefaultScheme, DefaultHost, DefaultPort, serverKey, adapter) { } public Client(string scheme, string host, int port, string serverKey) : this( scheme, host, port, serverKey, HttpRequestAdapter.WithGzip()) { } public Client(string scheme, string host, int port, string serverKey, IHttpAdapter adapter, bool autoRefreshSession = true) { AutoRefreshSession = autoRefreshSession; Host = host; Port = port; Scheme = scheme; ServerKey = serverKey; _apiClient = new ApiClient(new UriBuilder(scheme, host, port).Uri, adapter, DefaultTimeout); Logger = NullLogger.Instance; // must set logger last. _retryInvoker = new RetryInvoker(adapter.TransientExceptionDelegate); } public Client(Uri uri, string serverKey) : this( uri, serverKey, HttpRequestAdapter.WithGzip()) { } public Client(Uri uri, string serverKey, IHttpAdapter adapter, bool autoRefreshSession = true) { AutoRefreshSession = autoRefreshSession; Host = uri.Host; Port = uri.Port; Scheme = uri.Scheme; ServerKey = serverKey; _apiClient = new ApiClient(uri, adapter, DefaultTimeout); Logger = NullLogger.Instance; // must set logger last. _retryInvoker = new RetryInvoker(adapter.TransientExceptionDelegate); } /// public async Task AddFriendsAsync(ISession session, IEnumerable ids, IEnumerable usernames = null, string metadata = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.AddFriendsAsync(session.AuthToken, ids, usernames, metadata, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task AddGroupUsersAsync(ISession session, string groupId, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.AddGroupUsersAsync(session.AuthToken, groupId, ids, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task AuthenticateAppleAsync(string token, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.AuthenticateAppleAsync(ServerKey, string.Empty, new ApiAccountApple { Token = token, _vars = vars }, create, username, canceller), new RetryHistory(token, retryConfiguration ?? GlobalRetryConfiguration, canceller)); return new Session(response.Token, response.RefreshToken, response.Created); } /// public async Task AuthenticateCustomAsync(string id, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.AuthenticateCustomAsync(ServerKey, string.Empty, new ApiAccountCustom { Id = id, _vars = vars }, create, username, canceller), new RetryHistory(id, retryConfiguration ?? GlobalRetryConfiguration, canceller)); return new Session(response.Token, response.RefreshToken, response.Created); } /// public async Task AuthenticateDeviceAsync(string id, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.AuthenticateDeviceAsync(ServerKey, string.Empty, new ApiAccountDevice { Id = id, _vars = vars }, create, username, canceller), new RetryHistory(id, retryConfiguration ?? GlobalRetryConfiguration, canceller)); return new Session(response.Token, response.RefreshToken, response.Created); } /// public async Task AuthenticateEmailAsync(string email, string password, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.AuthenticateEmailAsync(ServerKey, string.Empty, new ApiAccountEmail { Email = email, Password = password, _vars = vars }, create, username, canceller), new RetryHistory(email, retryConfiguration ?? GlobalRetryConfiguration, canceller)); return new Session(response.Token, response.RefreshToken, response.Created); } /// public async Task AuthenticateFacebookAsync(string token, string username = null, bool create = true, bool import = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.AuthenticateFacebookAsync(ServerKey, string.Empty, new ApiAccountFacebook { Token = token, _vars = vars }, create, username, import, canceller), new RetryHistory(token, retryConfiguration ?? GlobalRetryConfiguration, canceller)); return new Session(response.Token, response.RefreshToken, response.Created); } /// public async Task AuthenticateGameCenterAsync(string bundleId, string playerId, string publicKeyUrl, string salt, string signature, string timestamp, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.AuthenticateGameCenterAsync(ServerKey, string.Empty, new ApiAccountGameCenter { BundleId = bundleId, PlayerId = playerId, PublicKeyUrl = publicKeyUrl, Salt = salt, Signature = signature, TimestampSeconds = timestamp, _vars = vars }, create, username, canceller), new RetryHistory(bundleId, retryConfiguration ?? GlobalRetryConfiguration, canceller)); return new Session(response.Token, response.RefreshToken, response.Created); } /// public async Task AuthenticateGoogleAsync(string token, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.AuthenticateGoogleAsync(ServerKey, string.Empty, new ApiAccountGoogle { Token = token, _vars = vars }, create, username, canceller), new RetryHistory(token, retryConfiguration ?? GlobalRetryConfiguration, canceller)); return new Session(response.Token, response.RefreshToken, response.Created); } /// public async Task AuthenticateSteamAsync(string token, string username = null, bool create = true, bool import = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.AuthenticateSteamAsync(ServerKey, string.Empty, new ApiAccountSteam { Token = token, _vars = vars }, create, username, import, canceller), new RetryHistory(token, retryConfiguration ?? GlobalRetryConfiguration, canceller)); return new Session(response.Token, response.RefreshToken, response.Created); } /// public async Task BanGroupUsersAsync(ISession session, string groupId, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.BanGroupUsersAsync(session.AuthToken, groupId, ids, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task BlockFriendsAsync(ISession session, IEnumerable ids, IEnumerable usernames = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.BlockFriendsAsync(session.AuthToken, ids, usernames, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task CreateGroupAsync(ISession session, string name, string description = "", string avatarUrl = null, string langTag = null, bool open = true, int maxCount = 100, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.CreateGroupAsync(session.AuthToken, new ApiCreateGroupRequest { Name = name, Description = description, AvatarUrl = avatarUrl, LangTag = langTag, Open = open, MaxCount = maxCount }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task DeleteAccountAsync(ISession session, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.DeleteAccountAsync(session.AuthToken, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task DeleteFriendsAsync(ISession session, IEnumerable ids, IEnumerable usernames = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.DeleteFriendsAsync(session.AuthToken, ids, usernames, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task DeleteGroupAsync(ISession session, string groupId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.DeleteGroupAsync(session.AuthToken, groupId, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task DeleteLeaderboardRecordAsync(ISession session, string leaderboardId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.DeleteLeaderboardRecordAsync(session.AuthToken, leaderboardId, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task DeleteNotificationsAsync(ISession session, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.DeleteNotificationsAsync(session.AuthToken, ids, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task DeleteStorageObjectsAsync(ISession session, StorageObjectId[] ids = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } var objects = new List(); if (ids != null) { foreach (var id in ids) { objects.Add(new ApiDeleteStorageObjectId { Collection = id.Collection, Key = id.Key, Version = id.Version }); } } await _retryInvoker.InvokeWithRetry(() => _apiClient.DeleteStorageObjectsAsync(session.AuthToken, new ApiDeleteStorageObjectsRequest { _objectIds = objects }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task DeleteTournamentRecordAsync(ISession session, string tournamentId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.DeleteTournamentRecordAsync(session.AuthToken, tournamentId, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task DemoteGroupUsersAsync(ISession session, string groupId, IEnumerable usernames, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.DemoteGroupUsersAsync(session.AuthToken, groupId, usernames, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task EventAsync(ISession session, string name, Dictionary properties, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.EventAsync(session.AuthToken, new ApiEvent() { External = true, Name = name, _properties = properties }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task GetAccountAsync(ISession session, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.GetAccountAsync(session.AuthToken, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task GetSubscriptionAsync(ISession session, string productId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.GetSubscriptionAsync(session.AuthToken, productId, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task GetUsersAsync(ISession session, IEnumerable ids, IEnumerable usernames = null, IEnumerable facebookIds = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.GetUsersAsync(session.AuthToken, ids, usernames, facebookIds, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ImportFacebookFriendsAsync(ISession session, string token, bool? reset = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.ImportFacebookFriendsAsync(session.AuthToken, new ApiAccountFacebook { Token = token }, reset, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ImportSteamFriendsAsync(ISession session, string token, bool? reset = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.ImportSteamFriendsAsync(session.AuthToken, new ApiAccountSteam { Token = token }, reset, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task JoinGroupAsync(ISession session, string groupId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.JoinGroupAsync(session.AuthToken, groupId, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task JoinTournamentAsync(ISession session, string tournamentId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.JoinTournamentAsync(session.AuthToken, tournamentId, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task KickGroupUsersAsync(ISession session, string groupId, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.KickGroupUsersAsync(session.AuthToken, groupId, ids, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task LeaveGroupAsync(ISession session, string groupId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.LeaveGroupAsync(session.AuthToken, groupId, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task LinkAppleAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.LinkAppleAsync(session.AuthToken, new ApiAccountApple { Token = token }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task LinkCustomAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.LinkCustomAsync(session.AuthToken, new ApiAccountCustom { Id = id }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task LinkDeviceAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.LinkDeviceAsync(session.AuthToken, new ApiAccountDevice { Id = id }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task LinkEmailAsync(ISession session, string email, string password, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.LinkEmailAsync(session.AuthToken, new ApiAccountEmail { Email = email, Password = password }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task LinkFacebookAsync(ISession session, string token, bool? import = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.LinkFacebookAsync(session.AuthToken, new ApiAccountFacebook { Token = token }, import, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task LinkGameCenterAsync(ISession session, string bundleId, string playerId, string publicKeyUrl, string salt, string signature, string timestamp, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.LinkGameCenterAsync(session.AuthToken, new ApiAccountGameCenter { BundleId = bundleId, PlayerId = playerId, PublicKeyUrl = publicKeyUrl, Salt = salt, Signature = signature, TimestampSeconds = timestamp }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task LinkGoogleAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.LinkGoogleAsync(session.AuthToken, new ApiAccountGoogle { Token = token }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task LinkSteamAsync(ISession session, string token, bool sync, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.LinkSteamAsync(session.AuthToken, new ApiLinkSteamRequest { Sync = sync, _account = new ApiAccountSteam { Token = token } }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public Task ListChannelMessagesAsync(ISession session, IChannel channel, int limit = 1, bool forward = true, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) => ListChannelMessagesAsync(session, channel.Id, limit, forward, cursor, retryConfiguration, canceller); /// public async Task ListChannelMessagesAsync(ISession session, string channelId, int limit = 1, bool forward = true, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.ListChannelMessagesAsync(session.AuthToken, channelId, limit, forward, cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ListFriendsAsync(ISession session, int? state, int limit, string cursor, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.ListFriendsAsync(session.AuthToken, limit, state, cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// /// /// The returned IApiGroupUserList is automatically updated to reflect username changes for the /// current user. /// public async Task ListGroupUsersAsync(ISession session, string groupId, int? state, int limit, string cursor, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } var response = await _retryInvoker.InvokeWithRetry( () => _apiClient.ListGroupUsersAsync(session.AuthToken, groupId, limit, state, cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); foreach (var groupUser in response.GroupUsers) { if (session.UserId.Equals(groupUser.User.Id) && groupUser.User is ApiUser u) { u.Username = session.Username; } } return response; } /// public async Task ListGroupsAsync(ISession session, string name = null, int limit = 1, string cursor = null, string langTag = null, int? members = null, bool? open = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.ListGroupsAsync(session.AuthToken, name, cursor, limit, langTag, members, open, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// /// /// The returned IApiLeaderboardRecordList is automatically updated to reflect username changes for the /// current user. /// public async Task ListLeaderboardRecordsAsync(ISession session, string leaderboardId, IEnumerable ownerIds = null, long? expiry = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.ListLeaderboardRecordsAsync( session.AuthToken, leaderboardId, ownerIds, limit, cursor, expiry?.ToString(), canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); foreach (var record in response.Records) { if (session.UserId.Equals(record.OwnerId) && record is ApiLeaderboardRecord r) { r.Username = session.Username; } } foreach (var record in response.OwnerRecords) { if (session.UserId.Equals(record.OwnerId) && record is ApiLeaderboardRecord r) { r.Username = session.Username; } } return response; } /// /// /// The returned IApiLeaderboardRecordList is automatically updated to reflect username changes for the /// current user. /// public async Task ListLeaderboardRecordsAroundOwnerAsync(ISession session, string leaderboardId, string ownerId, long? expiry = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.ListLeaderboardRecordsAroundOwnerAsync( session.AuthToken, leaderboardId, ownerId, limit, expiry?.ToString(), cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); foreach (var record in response.Records) { if (session.UserId.Equals(record.OwnerId) && record is ApiLeaderboardRecord r) { r.Username = session.Username; } } foreach (var record in response.OwnerRecords) { if (session.UserId.Equals(record.OwnerId) && record is ApiLeaderboardRecord r) { r.Username = session.Username; } } return response; } /// public async Task ListMatchesAsync(ISession session, int min, int max, int limit, bool authoritative, string label, string query, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.ListMatchesAsync(session.AuthToken, limit, authoritative, label, min, max, query, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// /// /// The returned IApiNotificationList is automatically updated to reflect username changes for the /// current user. /// public async Task ListNotificationsAsync(ISession session, int limit = 1, string cacheableCursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } var response = await _retryInvoker.InvokeWithRetry( () => _apiClient.ListNotificationsAsync(session.AuthToken, limit, cacheableCursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); return response; } /// public async Task ListPartiesAsync(ISession session, int limit, bool? open, string query = null, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.ListPartiesAsync(session.AuthToken, limit, open, query, cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } [Obsolete("ListStorageObjects is obsolete, please use ListStorageObjectsAsync instead.", true)] public Task ListStorageObjects(ISession session, string collection, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) => _retryInvoker.InvokeWithRetry( () => _apiClient.ListStorageObjectsAsync(session.AuthToken, collection, string.Empty, limit, cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); /// public async Task ListStorageObjectsAsync(ISession session, string collection, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) => await ListStorageObjectsAsync(session, collection, session.UserId ?? string.Empty, limit, cursor, retryConfiguration, canceller); // ReSharper disable once MemberCanBePrivate.Global -- overload can be called externally /// public async Task ListStorageObjectsAsync(ISession session, string collection, string userId = "", int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.ListStorageObjectsAsync(session.AuthToken, collection, userId, limit, cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ListSubscriptionsAsync(ISession session, int limit, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.ListSubscriptionsAsync(session.AuthToken, new ApiListSubscriptionsRequest { Cursor = cursor, Limit = limit }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// /// /// The returned IApiTournamentRecordList is automatically updated to reflect username changes for the /// current user. /// public async Task ListTournamentRecordsAroundOwnerAsync(ISession session, string tournamentId, string ownerId, long? expiry = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.ListTournamentRecordsAroundOwnerAsync( session.AuthToken, tournamentId, ownerId, limit, expiry?.ToString(), cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); foreach (var record in response.Records) { if (session.UserId.Equals(record.OwnerId) && record is ApiLeaderboardRecord r) { r.Username = session.Username; } } foreach (var record in response.OwnerRecords) { if (session.UserId.Equals(record.OwnerId) && record is ApiLeaderboardRecord r) { r.Username = session.Username; } } return response; } /// /// /// The returned IApiTournamentRecordList is automatically updated to reflect username changes for the /// current user. /// public async Task ListTournamentRecordsAsync(ISession session, string tournamentId, IEnumerable ownerIds = null, long? expiry = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.ListTournamentRecordsAsync( session.AuthToken, tournamentId, ownerIds, limit, cursor, expiry?.ToString(), canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); foreach (var record in response.Records) { if (session.UserId.Equals(record.OwnerId) && record is ApiLeaderboardRecord r) { r.Username = session.Username; } } foreach (var record in response.OwnerRecords) { if (session.UserId.Equals(record.OwnerId) && record is ApiLeaderboardRecord r) { r.Username = session.Username; } } return response; } /// public async Task ListTournamentsAsync(ISession session, int categoryStart, int categoryEnd, int? startTime = null, int? endTime = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.ListTournamentsAsync(session.AuthToken, categoryStart, categoryEnd, startTime, endTime, limit, cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public Task ListUserGroupsAsync(ISession session, int? state, int limit, string cursor, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) => ListUserGroupsAsync(session, session.UserId, state, limit, cursor); /// public async Task ListUserGroupsAsync(ISession session, string userId, int? state, int limit, string cursor, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.ListUserGroupsAsync(session.AuthToken, userId, limit, state, cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ListUsersStorageObjectsAsync(ISession session, string collection, string userId, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.ListStorageObjects2Async(session.AuthToken, collection, userId, limit, cursor, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task PromoteGroupUsersAsync(ISession session, string groupId, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.PromoteGroupUsersAsync(session.AuthToken, groupId, ids, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ReadStorageObjectsAsync(ISession session, IApiReadStorageObjectId[] ids = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } var objects = new List(); if (ids != null) { foreach (var id in ids) { objects.Add(new ApiReadStorageObjectId { Collection = id.Collection, Key = id.Key, UserId = id.UserId }); } } return await _retryInvoker.InvokeWithRetry(() => _apiClient.ReadStorageObjectsAsync(session.AuthToken, new ApiReadStorageObjectsRequest { _objectIds = objects }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task RpcAsync(ISession session, string id, string payload, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.RpcFuncAsync(session.AuthToken, string.Empty, string.Empty, id, payload, null, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task RpcAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.RpcFunc2Async(session.AuthToken, string.Empty, string.Empty, id, null, null, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public Task RpcAsync(string httpkey, string id, string payload, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) => _retryInvoker.InvokeWithRetry(() => _apiClient.RpcFuncAsync(null, string.Empty, string.Empty, id, payload, httpkey, canceller), new RetryHistory(id, retryConfiguration ?? GlobalRetryConfiguration, canceller)); /// public Task RpcAsync(string httpkey, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) => _retryInvoker.InvokeWithRetry(() => _apiClient.RpcFunc2Async(null, string.Empty, string.Empty, id, null, httpkey, canceller), new RetryHistory(id, retryConfiguration ?? GlobalRetryConfiguration, canceller)); /// public Task SessionLogoutAsync(ISession session, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) => SessionLogoutAsync(session.AuthToken, session.RefreshToken, retryConfiguration, canceller); /// public Task SessionLogoutAsync(string authToken, string refreshToken, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) => _retryInvoker.InvokeWithRetry(() => _apiClient.SessionLogoutAsync(authToken, new ApiSessionLogoutRequest { Token = authToken, RefreshToken = refreshToken }, canceller), new RetryHistory(authToken, retryConfiguration ?? GlobalRetryConfiguration, canceller)); /// public async Task SessionRefreshAsync(ISession session, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { // NOTE: Warn developers to encourage them to set a suitable session and refresh token lifetime. if (session.Created && session.ExpireTime - session.CreateTime < 70) { Logger.WarnFormat( "Session lifetime too short, please set '--session.token_expiry_sec' option. See the documentation for more info: https://heroiclabs.com/docs/install-configuration/#session"); } if (session.Created && session.RefreshExpireTime - session.CreateTime < 3700) { Logger.WarnFormat( "Session refresh lifetime too short, please set '--session.refresh_token_expiry_sec' option. See the documentation for more info: https://heroiclabs.com/docs/install-configuration/#session"); } var response = await _retryInvoker.InvokeWithRetry(() => _apiClient.SessionRefreshAsync(ServerKey, string.Empty, new ApiSessionRefreshRequest { Token = session.RefreshToken, _vars = vars }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); if (session is Session updatedSession) { // Update session object in place if we can. updatedSession.Update(response.Token, response.RefreshToken); ReceivedSessionUpdated?.Invoke(updatedSession); return updatedSession; } var newSession = new Session(response.Token, response.RefreshToken, response.Created); ReceivedSessionUpdated?.Invoke(newSession); return newSession; } public override string ToString() => $"Client(Host='{Host}', Port={Port}, Scheme='{Scheme}', ServerKey='{ServerKey}', Timeout={Timeout})"; /// public async Task UnlinkAppleAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.UnlinkAppleAsync(session.AuthToken, new ApiAccountApple { Token = token }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task UnlinkCustomAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.UnlinkCustomAsync(session.AuthToken, new ApiAccountCustom { Id = id }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task UnlinkDeviceAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.UnlinkDeviceAsync(session.AuthToken, new ApiAccountDevice { Id = id }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task UnlinkEmailAsync(ISession session, string email, string password, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.UnlinkEmailAsync(session.AuthToken, new ApiAccountEmail { Email = email, Password = password }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task UnlinkFacebookAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.UnlinkFacebookAsync(session.AuthToken, new ApiAccountFacebook { Token = token }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task UnlinkGameCenterAsync(ISession session, string bundleId, string playerId, string publicKeyUrl, string salt, string signature, string timestamp, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.UnlinkGameCenterAsync( session.AuthToken, new ApiAccountGameCenter { BundleId = bundleId, PlayerId = playerId, PublicKeyUrl = publicKeyUrl, Salt = salt, Signature = signature, TimestampSeconds = timestamp }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task UnlinkGoogleAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.UnlinkGoogleAsync(session.AuthToken, new ApiAccountGoogle { Token = token }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task UnlinkSteamAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry( () => _apiClient.UnlinkSteamAsync(session.AuthToken, new ApiAccountSteam { Token = token }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// /// /// The current user's username will be automatically refreshed in their authorized ISession if the /// username field is updated to become different. /// public async Task UpdateAccountAsync(ISession session, string username, string displayName = null, string avatarUrl = null, string langTag = null, string location = null, string timezone = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.UpdateAccountAsync( session.AuthToken, new ApiUpdateAccountRequest { AvatarUrl = avatarUrl, DisplayName = displayName, LangTag = langTag, Location = location, Timezone = timezone, Username = username }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); if (!String.IsNullOrEmpty(username) && !username.Equals(session.Username)) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } } /// public async Task UpdateGroupAsync(ISession session, string groupId, string name, bool open, string description = null, string avatarUrl = null, string langTag = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } await _retryInvoker.InvokeWithRetry(() => _apiClient.UpdateGroupAsync( session.AuthToken, groupId, new ApiUpdateGroupRequest() { Name = name, Open = open, AvatarUrl = avatarUrl, Description = description, LangTag = langTag }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ValidatePurchaseAppleAsync(ISession session, string receipt, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.ValidatePurchaseAppleAsync(session.AuthToken, new ApiValidatePurchaseAppleRequest { Receipt = receipt, Persist = persist }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ValidatePurchaseFacebookInstantAsync(ISession session, string signedRequest, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.ValidatePurchaseFacebookInstantAsync(session.AuthToken, new ApiValidatePurchaseFacebookInstantRequest { SignedRequest = signedRequest, Persist = persist }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ValidatePurchaseGoogleAsync(ISession session, string receipt, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.ValidatePurchaseGoogleAsync(session.AuthToken, new ApiValidatePurchaseGoogleRequest { Purchase = receipt, Persist = persist }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task ValidatePurchaseHuaweiAsync(ISession session, string receipt, string signature, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.ValidatePurchaseHuaweiAsync(session.AuthToken, new ApiValidatePurchaseHuaweiRequest { Purchase = receipt, Signature = signature, Persist = persist }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } public async Task ValidateSubscriptionAppleAsync(ISession session, string receipt, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.ValidateSubscriptionAppleAsync( session.AuthToken, new ApiValidateSubscriptionAppleRequest { Receipt = receipt, Persist = persist }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } public async Task ValidateSubscriptionGoogleAsync(ISession session, string receipt, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.ValidateSubscriptionGoogleAsync( session.AuthToken, new ApiValidateSubscriptionGoogleRequest { Receipt = receipt, Persist = persist }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task WriteLeaderboardRecordAsync(ISession session, string leaderboardId, long score, long subScore = 0, string metadata = null, ApiOperator apiOperator = ApiOperator.NO_OVERRIDE, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.WriteLeaderboardRecordAsync( session.AuthToken, leaderboardId, new WriteLeaderboardRecordRequestLeaderboardRecordWrite { Metadata = metadata, Score = score.ToString(), Subscore = subScore.ToString(), _operator = apiOperator }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task WriteStorageObjectsAsync(ISession session, IApiWriteStorageObject[] objects = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } var writes = new List(objects.Length); foreach (var obj in objects) { writes.Add(new ApiWriteStorageObject { Collection = obj.Collection, Key = obj.Key, PermissionRead = obj.PermissionRead, PermissionWrite = obj.PermissionWrite, Value = obj.Value, Version = obj.Version }); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.WriteStorageObjectsAsync(session.AuthToken, new ApiWriteStorageObjectsRequest { _objects = writes }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } /// public async Task WriteTournamentRecordAsync(ISession session, string tournamentId, long score, long subScore = 0, string metadata = null, ApiOperator apiOperator = ApiOperator.NO_OVERRIDE, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, null, retryConfiguration, canceller); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.WriteTournamentRecordAsync(session.AuthToken, tournamentId, new WriteTournamentRecordRequestTournamentRecordWrite { Metadata = metadata, Score = score.ToString(), Subscore = subScore.ToString(), _operator = apiOperator }, canceller), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, canceller)); } } } ================================================ FILE: Nakama/Console/ConsoleClient.gen.cs ================================================ /* Code generated by codegen/main.go. DO NOT EDIT. */ namespace Nakama.Console { using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; using TinyJson; /// /// An exception generated for HttpResponse objects don't return a success status. /// public sealed class ApiResponseException : Exception { public long StatusCode { get; } public int GrpcStatusCode { get; } public ApiResponseException(long statusCode, string content, int grpcCode) : base(content) { StatusCode = statusCode; GrpcStatusCode = grpcCode; } public ApiResponseException(string message, Exception e) : base(message, e) { StatusCode = -1L; GrpcStatusCode = -1; } public ApiResponseException(string content) : this(-1L, content, -1) { } public override string ToString() { return $"ApiResponseException(StatusCode={StatusCode}, Message='{Message}', GrpcStatusCode={GrpcStatusCode})"; } } /// /// Add/join users to a group. /// public interface IApiConsole_AddGroupUsersRequest { /// /// Users to add/join. /// string Ids { get; } /// /// Whether it is a join request. /// bool JoinRequest { get; } } /// internal class ApiConsole_AddGroupUsersRequest : IApiConsole_AddGroupUsersRequest { /// [DataMember(Name="ids"), Preserve] public string Ids { get; set; } /// [DataMember(Name="join_request"), Preserve] public bool JoinRequest { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Ids: ", Ids, ", "); output = string.Concat(output, "JoinRequest: ", JoinRequest, ", "); return output; } } /// /// /// public interface IApiConsole_CallApiEndpointRequest { /// /// /// string Body { get; } /// /// /// IDictionary SessionVars { get; } /// /// /// string UserId { get; } } /// internal class ApiConsole_CallApiEndpointRequest : IApiConsole_CallApiEndpointRequest { /// [DataMember(Name="body"), Preserve] public string Body { get; set; } /// [IgnoreDataMember] public IDictionary SessionVars => _sessionVars ?? new Dictionary(); [DataMember(Name="session_vars"), Preserve] public Dictionary _sessionVars { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Body: ", Body, ", "); var session_varsString = ""; foreach (var kvp in SessionVars) { session_varsString = string.Concat(session_varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "SessionVars: [" + session_varsString + "]"); output = string.Concat(output, "UserId: ", UserId, ", "); return output; } } /// /// /// public interface IApiConsole_CallRpcEndpointRequest { /// /// /// string Body { get; } /// /// /// IDictionary SessionVars { get; } /// /// /// string UserId { get; } } /// internal class ApiConsole_CallRpcEndpointRequest : IApiConsole_CallRpcEndpointRequest { /// [DataMember(Name="body"), Preserve] public string Body { get; set; } /// [IgnoreDataMember] public IDictionary SessionVars => _sessionVars ?? new Dictionary(); [DataMember(Name="session_vars"), Preserve] public Dictionary _sessionVars { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Body: ", Body, ", "); var session_varsString = ""; foreach (var kvp in SessionVars) { session_varsString = string.Concat(session_varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "SessionVars: [" + session_varsString + "]"); output = string.Concat(output, "UserId: ", UserId, ", "); return output; } } /// /// Make a user's mfa required or not. /// public interface IApiConsole_RequireUserMfaRequest { /// /// Required. /// bool Required { get; } } /// internal class ApiConsole_RequireUserMfaRequest : IApiConsole_RequireUserMfaRequest { /// [DataMember(Name="required"), Preserve] public bool Required { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Required: ", Required, ", "); return output; } } /// /// Unlink a particular device ID from a user's account. /// public interface IApiConsole_UnlinkDeviceRequest { /// /// Device ID to unlink. /// string DeviceId { get; } } /// internal class ApiConsole_UnlinkDeviceRequest : IApiConsole_UnlinkDeviceRequest { /// [DataMember(Name="device_id"), Preserve] public string DeviceId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "DeviceId: ", DeviceId, ", "); return output; } } /// /// Update user account information. /// public interface IApiConsole_UpdateAccountRequest { /// /// Avatar URL. /// string AvatarUrl { get; } /// /// Custom ID. /// string CustomId { get; } /// /// Device ID modifications. /// IDictionary DeviceIds { get; } /// /// Display name. /// string DisplayName { get; } /// /// Email. /// string Email { get; } /// /// Langtag. /// string LangTag { get; } /// /// Location. /// string Location { get; } /// /// Metadata. /// string Metadata { get; } /// /// Password. /// string Password { get; } /// /// Timezone. /// string Timezone { get; } /// /// Username. /// string Username { get; } /// /// Wallet. /// string Wallet { get; } } /// internal class ApiConsole_UpdateAccountRequest : IApiConsole_UpdateAccountRequest { /// [DataMember(Name="avatar_url"), Preserve] public string AvatarUrl { get; set; } /// [DataMember(Name="custom_id"), Preserve] public string CustomId { get; set; } /// [IgnoreDataMember] public IDictionary DeviceIds => _deviceIds ?? new Dictionary(); [DataMember(Name="device_ids"), Preserve] public Dictionary _deviceIds { get; set; } /// [DataMember(Name="display_name"), Preserve] public string DisplayName { get; set; } /// [DataMember(Name="email"), Preserve] public string Email { get; set; } /// [DataMember(Name="lang_tag"), Preserve] public string LangTag { get; set; } /// [DataMember(Name="location"), Preserve] public string Location { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="password"), Preserve] public string Password { get; set; } /// [DataMember(Name="timezone"), Preserve] public string Timezone { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } /// [DataMember(Name="wallet"), Preserve] public string Wallet { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AvatarUrl: ", AvatarUrl, ", "); output = string.Concat(output, "CustomId: ", CustomId, ", "); var device_idsString = ""; foreach (var kvp in DeviceIds) { device_idsString = string.Concat(device_idsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "DeviceIds: [" + device_idsString + "]"); output = string.Concat(output, "DisplayName: ", DisplayName, ", "); output = string.Concat(output, "Email: ", Email, ", "); output = string.Concat(output, "LangTag: ", LangTag, ", "); output = string.Concat(output, "Location: ", Location, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "Password: ", Password, ", "); output = string.Concat(output, "Timezone: ", Timezone, ", "); output = string.Concat(output, "Username: ", Username, ", "); output = string.Concat(output, "Wallet: ", Wallet, ", "); return output; } } /// /// Update group information. /// public interface IApiConsole_UpdateGroupRequest { /// /// Avatar URL. /// string AvatarUrl { get; } /// /// Description. /// string Description { get; } /// /// Langtag. /// string LangTag { get; } /// /// The maximum number of members allowed. /// int MaxCount { get; } /// /// Metadata. /// string Metadata { get; } /// /// Name. /// string Name { get; } /// /// Anyone can join open groups, otherwise only admins can accept members. /// bool Open { get; } } /// internal class ApiConsole_UpdateGroupRequest : IApiConsole_UpdateGroupRequest { /// [DataMember(Name="avatar_url"), Preserve] public string AvatarUrl { get; set; } /// [DataMember(Name="description"), Preserve] public string Description { get; set; } /// [DataMember(Name="lang_tag"), Preserve] public string LangTag { get; set; } /// [DataMember(Name="max_count"), Preserve] public int MaxCount { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="open"), Preserve] public bool Open { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AvatarUrl: ", AvatarUrl, ", "); output = string.Concat(output, "Description: ", Description, ", "); output = string.Concat(output, "LangTag: ", LangTag, ", "); output = string.Concat(output, "MaxCount: ", MaxCount, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "Open: ", Open, ", "); return output; } } /// /// Request to update an existing setting. /// public interface IApiConsole_UpdateSettingRequest { /// /// Setting value. /// string Value { get; } } /// internal class ApiConsole_UpdateSettingRequest : IApiConsole_UpdateSettingRequest { /// [DataMember(Name="value"), Preserve] public string Value { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Value: ", Value, ", "); return output; } } /// /// Write a new storage object or update an existing one. /// public interface IApiConsole_WriteStorageObjectRequest { /// /// Read permission value. /// int PermissionRead { get; } /// /// Write permission value. /// int PermissionWrite { get; } /// /// Value. /// string Value { get; } /// /// Version for OCC. /// string Version { get; } } /// internal class ApiConsole_WriteStorageObjectRequest : IApiConsole_WriteStorageObjectRequest { /// [DataMember(Name="permission_read"), Preserve] public int PermissionRead { get; set; } /// [DataMember(Name="permission_write"), Preserve] public int PermissionWrite { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } /// [DataMember(Name="version"), Preserve] public string Version { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "PermissionRead: ", PermissionRead, ", "); output = string.Concat(output, "PermissionWrite: ", PermissionWrite, ", "); output = string.Concat(output, "Value: ", Value, ", "); output = string.Concat(output, "Version: ", Version, ", "); return output; } } /// /// A warning for a configuration field. /// public interface IConfigWarning { /// /// The config field this warning is for in a JSON pointer format. /// string Field { get; } /// /// Warning message text. /// string Message { get; } } /// internal class ConfigWarning : IConfigWarning { /// [DataMember(Name="field"), Preserve] public string Field { get; set; } /// [DataMember(Name="message"), Preserve] public string Message { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Field: ", Field, ", "); output = string.Concat(output, "Message: ", Message, ", "); return output; } } /// /// A single user-role pair. /// public interface IGroupUserListGroupUser { /// /// Their relationship to the group. /// int State { get; } /// /// User. /// INakamaapiUser User { get; } } /// internal class GroupUserListGroupUser : IGroupUserListGroupUser { /// [DataMember(Name="state"), Preserve] public int State { get; set; } /// [IgnoreDataMember] public INakamaapiUser User => _user; [DataMember(Name="user"), Preserve] public NakamaapiUser _user { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "State: ", State, ", "); output = string.Concat(output, "User: ", User, ", "); return output; } } /// /// Module information /// public interface IRuntimeInfoModuleInfo { /// /// Module last modified date /// string ModTime { get; } /// /// Module path /// string Path { get; } } /// internal class RuntimeInfoModuleInfo : IRuntimeInfoModuleInfo { /// [DataMember(Name="mod_time"), Preserve] public string ModTime { get; set; } /// [DataMember(Name="path"), Preserve] public string Path { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ModTime: ", ModTime, ", "); output = string.Concat(output, "Path: ", Path, ", "); return output; } } /// /// A single group-role pair. /// public interface IUserGroupListUserGroup { /// /// Group. /// IApiGroup Group { get; } /// /// The user's relationship to the group. /// int State { get; } } /// internal class UserGroupListUserGroup : IUserGroupListUserGroup { /// [IgnoreDataMember] public IApiGroup Group => _group; [DataMember(Name="group"), Preserve] public ApiGroup _group { get; set; } /// [DataMember(Name="state"), Preserve] public int State { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Group: ", Group, ", "); output = string.Concat(output, "State: ", State, ", "); return output; } } /// /// Send a device to the server. Used with authenticate/link/unlink and user. /// public interface IApiAccountDevice { /// /// A device identifier. Should be obtained by a platform-specific device API. /// string Id { get; } /// /// Extra information that will be bundled in the session token. /// IDictionary Vars { get; } } /// internal class ApiAccountDevice : IApiAccountDevice { /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [IgnoreDataMember] public IDictionary Vars => _vars ?? new Dictionary(); [DataMember(Name="vars"), Preserve] public Dictionary _vars { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Id: ", Id, ", "); var varsString = ""; foreach (var kvp in Vars) { varsString = string.Concat(varsString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Vars: [" + varsString + "]"); return output; } } /// /// A message sent on a channel. /// public interface IApiChannelMessage { /// /// The channel this message belongs to. /// string ChannelId { get; } /// /// The code representing a message type or category. /// int Code { get; } /// /// The content payload. /// string Content { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the message was created. /// string CreateTime { get; } /// /// The ID of the group, or an empty string if this message was not sent through a group channel. /// string GroupId { get; } /// /// The unique ID of this message. /// string MessageId { get; } /// /// True if the message was persisted to the channel's history, false otherwise. /// bool Persistent { get; } /// /// The name of the chat room, or an empty string if this message was not sent through a chat room. /// string RoomName { get; } /// /// Message sender, usually a user ID. /// string SenderId { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the message was last updated. /// string UpdateTime { get; } /// /// The ID of the first DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdOne { get; } /// /// The ID of the second DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdTwo { get; } /// /// The username of the message sender, if any. /// string Username { get; } } /// internal class ApiChannelMessage : IApiChannelMessage { /// [DataMember(Name="channel_id"), Preserve] public string ChannelId { get; set; } /// [DataMember(Name="code"), Preserve] public int Code { get; set; } /// [DataMember(Name="content"), Preserve] public string Content { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="group_id"), Preserve] public string GroupId { get; set; } /// [DataMember(Name="message_id"), Preserve] public string MessageId { get; set; } /// [DataMember(Name="persistent"), Preserve] public bool Persistent { get; set; } /// [DataMember(Name="room_name"), Preserve] public string RoomName { get; set; } /// [DataMember(Name="sender_id"), Preserve] public string SenderId { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id_one"), Preserve] public string UserIdOne { get; set; } /// [DataMember(Name="user_id_two"), Preserve] public string UserIdTwo { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ChannelId: ", ChannelId, ", "); output = string.Concat(output, "Code: ", Code, ", "); output = string.Concat(output, "Content: ", Content, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "GroupId: ", GroupId, ", "); output = string.Concat(output, "MessageId: ", MessageId, ", "); output = string.Concat(output, "Persistent: ", Persistent, ", "); output = string.Concat(output, "RoomName: ", RoomName, ", "); output = string.Concat(output, "SenderId: ", SenderId, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserIdOne: ", UserIdOne, ", "); output = string.Concat(output, "UserIdTwo: ", UserIdTwo, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// A list of channel messages, usually a result of a list operation. /// public interface IApiChannelMessageList { /// /// Cacheable cursor to list newer messages. Durable and designed to be stored, unlike next/prev cursors. /// string CacheableCursor { get; } /// /// A list of messages. /// IEnumerable Messages { get; } /// /// The cursor to send when retrieving the next page, if any. /// string NextCursor { get; } /// /// The cursor to send when retrieving the previous page, if any. /// string PrevCursor { get; } } /// internal class ApiChannelMessageList : IApiChannelMessageList { /// [DataMember(Name="cacheable_cursor"), Preserve] public string CacheableCursor { get; set; } /// [IgnoreDataMember] public IEnumerable Messages => _messages ?? new List(0); [DataMember(Name="messages"), Preserve] public List _messages { get; set; } /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CacheableCursor: ", CacheableCursor, ", "); output = string.Concat(output, "Messages: [", string.Join(", ", Messages), "], "); output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); return output; } } /// /// A friend of a user. /// public interface IApiFriend { /// /// Metadata. /// string Metadata { get; } /// /// The friend status. one of "Friend.State". /// int State { get; } /// /// Time of the latest relationship update. /// string UpdateTime { get; } /// /// The user object. /// INakamaapiUser User { get; } } /// internal class ApiFriend : IApiFriend { /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="state"), Preserve] public int State { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [IgnoreDataMember] public INakamaapiUser User => _user; [DataMember(Name="user"), Preserve] public NakamaapiUser _user { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "State: ", State, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "User: ", User, ", "); return output; } } /// /// A collection of zero or more friends of the user. /// public interface IApiFriendList { /// /// Cursor for the next page of results, if any. /// string Cursor { get; } /// /// The Friend objects. /// IEnumerable Friends { get; } } /// internal class ApiFriendList : IApiFriendList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable Friends => _friends ?? new List(0); [DataMember(Name="friends"), Preserve] public List _friends { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "Friends: [", string.Join(", ", Friends), "], "); return output; } } /// /// A group in the server. /// public interface IApiGroup { /// /// A URL for an avatar image. /// string AvatarUrl { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the group was created. /// string CreateTime { get; } /// /// The id of the user who created the group. /// string CreatorId { get; } /// /// A description for the group. /// string Description { get; } /// /// The current count of all members in the group. /// int EdgeCount { get; } /// /// The id of a group. /// string Id { get; } /// /// The language expected to be a tag which follows the BCP-47 spec. /// string LangTag { get; } /// /// The maximum number of members allowed. /// int MaxCount { get; } /// /// Additional information stored as a JSON object. /// string Metadata { get; } /// /// The unique name of the group. /// string Name { get; } /// /// Anyone can join open groups, otherwise only admins can accept members. /// bool Open { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the group was last updated. /// string UpdateTime { get; } } /// internal class ApiGroup : IApiGroup { /// [DataMember(Name="avatar_url"), Preserve] public string AvatarUrl { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="creator_id"), Preserve] public string CreatorId { get; set; } /// [DataMember(Name="description"), Preserve] public string Description { get; set; } /// [DataMember(Name="edge_count"), Preserve] public int EdgeCount { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="lang_tag"), Preserve] public string LangTag { get; set; } /// [DataMember(Name="max_count"), Preserve] public int MaxCount { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="open"), Preserve] public bool Open { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AvatarUrl: ", AvatarUrl, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "CreatorId: ", CreatorId, ", "); output = string.Concat(output, "Description: ", Description, ", "); output = string.Concat(output, "EdgeCount: ", EdgeCount, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "LangTag: ", LangTag, ", "); output = string.Concat(output, "MaxCount: ", MaxCount, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "Open: ", Open, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); return output; } } /// /// A list of users belonging to a group, along with their role. /// public interface IApiGroupUserList { /// /// Cursor for the next page of results, if any. /// string Cursor { get; } /// /// User-role pairs for a group. /// IEnumerable GroupUsers { get; } } /// internal class ApiGroupUserList : IApiGroupUserList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable GroupUsers => _groupUsers ?? new List(0); [DataMember(Name="group_users"), Preserve] public List _groupUsers { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "GroupUsers: [", string.Join(", ", GroupUsers), "], "); return output; } } /// /// Represents a complete leaderboard record with all scores and associated metadata. /// public interface IApiLeaderboardRecord { /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard record was created. /// string CreateTime { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard record expires. /// string ExpiryTime { get; } /// /// The ID of the leaderboard this score belongs to. /// string LeaderboardId { get; } /// /// The maximum number of score updates allowed by the owner. /// int MaxNumScore { get; } /// /// Metadata. /// string Metadata { get; } /// /// The number of submissions to this score record. /// int NumScore { get; } /// /// The ID of the score owner, usually a user or group. /// string OwnerId { get; } /// /// The rank of this record. /// string Rank { get; } /// /// The score value. /// string Score { get; } /// /// An optional subscore value. /// string Subscore { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the leaderboard record was updated. /// string UpdateTime { get; } /// /// The username of the score owner, if the owner is a user. /// string Username { get; } } /// internal class ApiLeaderboardRecord : IApiLeaderboardRecord { /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="expiry_time"), Preserve] public string ExpiryTime { get; set; } /// [DataMember(Name="leaderboard_id"), Preserve] public string LeaderboardId { get; set; } /// [DataMember(Name="max_num_score"), Preserve] public int MaxNumScore { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="num_score"), Preserve] public int NumScore { get; set; } /// [DataMember(Name="owner_id"), Preserve] public string OwnerId { get; set; } /// [DataMember(Name="rank"), Preserve] public string Rank { get; set; } /// [DataMember(Name="score"), Preserve] public string Score { get; set; } /// [DataMember(Name="subscore"), Preserve] public string Subscore { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "ExpiryTime: ", ExpiryTime, ", "); output = string.Concat(output, "LeaderboardId: ", LeaderboardId, ", "); output = string.Concat(output, "MaxNumScore: ", MaxNumScore, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "NumScore: ", NumScore, ", "); output = string.Concat(output, "OwnerId: ", OwnerId, ", "); output = string.Concat(output, "Rank: ", Rank, ", "); output = string.Concat(output, "Score: ", Score, ", "); output = string.Concat(output, "Subscore: ", Subscore, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// A set of leaderboard records, may be part of a leaderboard records page or a batch of individual records. /// public interface IApiLeaderboardRecordList { /// /// The cursor to send when retrieving the next page, if any. /// string NextCursor { get; } /// /// A batched set of leaderboard records belonging to specified owners. /// IEnumerable OwnerRecords { get; } /// /// The cursor to send when retrieving the previous page, if any. /// string PrevCursor { get; } /// /// The total number of ranks available. /// string RankCount { get; } /// /// A list of leaderboard records. /// IEnumerable Records { get; } } /// internal class ApiLeaderboardRecordList : IApiLeaderboardRecordList { /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [IgnoreDataMember] public IEnumerable OwnerRecords => _ownerRecords ?? new List(0); [DataMember(Name="owner_records"), Preserve] public List _ownerRecords { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } /// [DataMember(Name="rank_count"), Preserve] public string RankCount { get; set; } /// [IgnoreDataMember] public IEnumerable Records => _records ?? new List(0); [DataMember(Name="records"), Preserve] public List _records { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "OwnerRecords: [", string.Join(", ", OwnerRecords), "], "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); output = string.Concat(output, "RankCount: ", RankCount, ", "); output = string.Concat(output, "Records: [", string.Join(", ", Records), "], "); return output; } } /// /// A list of validated purchases stored by Nakama. /// public interface IApiPurchaseList { /// /// The cursor to send when retrieving the next page, if any. /// string Cursor { get; } /// /// The cursor to send when retrieving the previous page, if any. /// string PrevCursor { get; } /// /// Stored validated purchases. /// IEnumerable ValidatedPurchases { get; } } /// internal class ApiPurchaseList : IApiPurchaseList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } /// [IgnoreDataMember] public IEnumerable ValidatedPurchases => _validatedPurchases ?? new List(0); [DataMember(Name="validated_purchases"), Preserve] public List _validatedPurchases { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); output = string.Concat(output, "ValidatedPurchases: [", string.Join(", ", ValidatedPurchases), "], "); return output; } } /// /// An object within the storage engine. /// public interface IApiStorageObject { /// /// The collection which stores the object. /// string Collection { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was created. /// string CreateTime { get; } /// /// The key of the object within the collection. /// string Key { get; } /// /// The read access permissions for the object. /// int PermissionRead { get; } /// /// The write access permissions for the object. /// int PermissionWrite { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was last updated. /// string UpdateTime { get; } /// /// The user owner of the object. /// string UserId { get; } /// /// The value of the object. /// string Value { get; } /// /// The version hash of the object. /// string Version { get; } } /// internal class ApiStorageObject : IApiStorageObject { /// [DataMember(Name="collection"), Preserve] public string Collection { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="key"), Preserve] public string Key { get; set; } /// [DataMember(Name="permission_read"), Preserve] public int PermissionRead { get; set; } /// [DataMember(Name="permission_write"), Preserve] public int PermissionWrite { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } /// [DataMember(Name="version"), Preserve] public string Version { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Collection: ", Collection, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Key: ", Key, ", "); output = string.Concat(output, "PermissionRead: ", PermissionRead, ", "); output = string.Concat(output, "PermissionWrite: ", PermissionWrite, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); output = string.Concat(output, "Value: ", Value, ", "); output = string.Concat(output, "Version: ", Version, ", "); return output; } } /// /// A storage acknowledgement. /// public interface IApiStorageObjectAck { /// /// The collection which stores the object. /// string Collection { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was created. /// string CreateTime { get; } /// /// The key of the object within the collection. /// string Key { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was last updated. /// string UpdateTime { get; } /// /// The owner of the object. /// string UserId { get; } /// /// The version hash of the object. /// string Version { get; } } /// internal class ApiStorageObjectAck : IApiStorageObjectAck { /// [DataMember(Name="collection"), Preserve] public string Collection { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="key"), Preserve] public string Key { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } /// [DataMember(Name="version"), Preserve] public string Version { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Collection: ", Collection, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Key: ", Key, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); output = string.Concat(output, "Version: ", Version, ", "); return output; } } /// /// Environment where a purchase/subscription took place, /// public enum ApiStoreEnvironment { /// /// - UNKNOWN: Unknown environment. /// UNKNOWN = 0, /// /// - SANDBOX: Sandbox/test environment. /// SANDBOX = 1, /// /// - PRODUCTION: Production environment. /// PRODUCTION = 2, } /// /// Validation Provider, /// public enum ApiStoreProvider { /// /// - APPLE_APP_STORE: Apple App Store /// APPLE_APP_STORE = 0, /// /// - GOOGLE_PLAY_STORE: Google Play Store /// GOOGLE_PLAY_STORE = 1, /// /// - HUAWEI_APP_GALLERY: Huawei App Gallery /// HUAWEI_APP_GALLERY = 2, /// /// - FACEBOOK_INSTANT_STORE: Facebook Instant Store /// FACEBOOK_INSTANT_STORE = 3, } /// /// A list of validated subscriptions stored by Nakama. /// public interface IApiSubscriptionList { /// /// The cursor to send when retrieving the next page, if any. /// string Cursor { get; } /// /// The cursor to send when retrieving the previous page, if any. /// string PrevCursor { get; } /// /// Stored validated subscriptions. /// IEnumerable ValidatedSubscriptions { get; } } /// internal class ApiSubscriptionList : IApiSubscriptionList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } /// [IgnoreDataMember] public IEnumerable ValidatedSubscriptions => _validatedSubscriptions ?? new List(0); [DataMember(Name="validated_subscriptions"), Preserve] public List _validatedSubscriptions { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); output = string.Concat(output, "ValidatedSubscriptions: [", string.Join(", ", ValidatedSubscriptions), "], "); return output; } } /// /// A list of groups belonging to a user, along with the user's role in each group. /// public interface IApiUserGroupList { /// /// Cursor for the next page of results, if any. /// string Cursor { get; } /// /// Group-role pairs for a user. /// IEnumerable UserGroups { get; } } /// internal class ApiUserGroupList : IApiUserGroupList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable UserGroups => _userGroups ?? new List(0); [DataMember(Name="user_groups"), Preserve] public List _userGroups { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "UserGroups: [", string.Join(", ", UserGroups), "], "); return output; } } /// /// Validated Purchase stored by Nakama. /// public interface IApiValidatedPurchase { /// /// Timestamp when the receipt validation was stored in DB. /// string CreateTime { get; } /// /// Whether the purchase was done in production or sandbox environment. /// ApiStoreEnvironment Environment { get; } /// /// Purchase Product ID. /// string ProductId { get; } /// /// Raw provider validation response. /// string ProviderResponse { get; } /// /// Timestamp when the purchase was done. /// string PurchaseTime { get; } /// /// Timestamp when the purchase was refunded. Set to UNIX /// string RefundTime { get; } /// /// Whether the purchase had already been validated by Nakama before. /// bool SeenBefore { get; } /// /// Store identifier /// ApiStoreProvider Store { get; } /// /// Purchase Transaction ID. /// string TransactionId { get; } /// /// Timestamp when the receipt validation was updated in DB. /// string UpdateTime { get; } /// /// Purchase User ID. /// string UserId { get; } } /// internal class ApiValidatedPurchase : IApiValidatedPurchase { /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [IgnoreDataMember] public ApiStoreEnvironment Environment => _environment; [DataMember(Name="environment"), Preserve] public ApiStoreEnvironment _environment { get; set; } /// [DataMember(Name="product_id"), Preserve] public string ProductId { get; set; } /// [DataMember(Name="provider_response"), Preserve] public string ProviderResponse { get; set; } /// [DataMember(Name="purchase_time"), Preserve] public string PurchaseTime { get; set; } /// [DataMember(Name="refund_time"), Preserve] public string RefundTime { get; set; } /// [DataMember(Name="seen_before"), Preserve] public bool SeenBefore { get; set; } /// [IgnoreDataMember] public ApiStoreProvider Store => _store; [DataMember(Name="store"), Preserve] public ApiStoreProvider _store { get; set; } /// [DataMember(Name="transaction_id"), Preserve] public string TransactionId { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Environment: ", Environment, ", "); output = string.Concat(output, "ProductId: ", ProductId, ", "); output = string.Concat(output, "ProviderResponse: ", ProviderResponse, ", "); output = string.Concat(output, "PurchaseTime: ", PurchaseTime, ", "); output = string.Concat(output, "RefundTime: ", RefundTime, ", "); output = string.Concat(output, "SeenBefore: ", SeenBefore, ", "); output = string.Concat(output, "Store: ", Store, ", "); output = string.Concat(output, "TransactionId: ", TransactionId, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); return output; } } /// /// /// public interface IApiValidatedSubscription { /// /// Whether the subscription is currently active or not. /// bool Active { get; } /// /// UNIX Timestamp when the receipt validation was stored in DB. /// string CreateTime { get; } /// /// Whether the purchase was done in production or sandbox environment. /// ApiStoreEnvironment Environment { get; } /// /// Subscription expiration time. The subscription can still be auto-renewed to extend the expiration time further. /// string ExpiryTime { get; } /// /// Purchase Original transaction ID (we only keep track of the original subscription, not subsequent renewals). /// string OriginalTransactionId { get; } /// /// Purchase Product ID. /// string ProductId { get; } /// /// Raw provider notification body. /// string ProviderNotification { get; } /// /// Raw provider validation response body. /// string ProviderResponse { get; } /// /// UNIX Timestamp when the purchase was done. /// string PurchaseTime { get; } /// /// Subscription refund time. If this time is set, the subscription was refunded. /// string RefundTime { get; } /// /// Store identifier /// ApiStoreProvider Store { get; } /// /// UNIX Timestamp when the receipt validation was updated in DB. /// string UpdateTime { get; } /// /// Subscription User ID. /// string UserId { get; } } /// internal class ApiValidatedSubscription : IApiValidatedSubscription { /// [DataMember(Name="active"), Preserve] public bool Active { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [IgnoreDataMember] public ApiStoreEnvironment Environment => _environment; [DataMember(Name="environment"), Preserve] public ApiStoreEnvironment _environment { get; set; } /// [DataMember(Name="expiry_time"), Preserve] public string ExpiryTime { get; set; } /// [DataMember(Name="original_transaction_id"), Preserve] public string OriginalTransactionId { get; set; } /// [DataMember(Name="product_id"), Preserve] public string ProductId { get; set; } /// [DataMember(Name="provider_notification"), Preserve] public string ProviderNotification { get; set; } /// [DataMember(Name="provider_response"), Preserve] public string ProviderResponse { get; set; } /// [DataMember(Name="purchase_time"), Preserve] public string PurchaseTime { get; set; } /// [DataMember(Name="refund_time"), Preserve] public string RefundTime { get; set; } /// [IgnoreDataMember] public ApiStoreProvider Store => _store; [DataMember(Name="store"), Preserve] public ApiStoreProvider _store { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Active: ", Active, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Environment: ", Environment, ", "); output = string.Concat(output, "ExpiryTime: ", ExpiryTime, ", "); output = string.Concat(output, "OriginalTransactionId: ", OriginalTransactionId, ", "); output = string.Concat(output, "ProductId: ", ProductId, ", "); output = string.Concat(output, "ProviderNotification: ", ProviderNotification, ", "); output = string.Concat(output, "ProviderResponse: ", ProviderResponse, ", "); output = string.Concat(output, "PurchaseTime: ", PurchaseTime, ", "); output = string.Concat(output, "RefundTime: ", RefundTime, ", "); output = string.Concat(output, "Store: ", Store, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); return output; } } /// /// An export of all information stored for a user account. /// public interface IConsoleAccountExport { /// /// The user's account details. /// INakamaapiAccount Account { get; } /// /// The user's friends. /// IEnumerable Friends { get; } /// /// The user's groups. /// IEnumerable Groups { get; } /// /// The user's leaderboard records. /// IEnumerable LeaderboardRecords { get; } /// /// The user's chat messages. /// IEnumerable Messages { get; } /// /// The user's notifications. /// IEnumerable Notifications { get; } /// /// The user's storage. /// IEnumerable Objects { get; } /// /// The user's wallet ledger items. /// IEnumerable WalletLedgers { get; } } /// internal class ConsoleAccountExport : IConsoleAccountExport { /// [IgnoreDataMember] public INakamaapiAccount Account => _account; [DataMember(Name="account"), Preserve] public NakamaapiAccount _account { get; set; } /// [IgnoreDataMember] public IEnumerable Friends => _friends ?? new List(0); [DataMember(Name="friends"), Preserve] public List _friends { get; set; } /// [IgnoreDataMember] public IEnumerable Groups => _groups ?? new List(0); [DataMember(Name="groups"), Preserve] public List _groups { get; set; } /// [IgnoreDataMember] public IEnumerable LeaderboardRecords => _leaderboardRecords ?? new List(0); [DataMember(Name="leaderboard_records"), Preserve] public List _leaderboardRecords { get; set; } /// [IgnoreDataMember] public IEnumerable Messages => _messages ?? new List(0); [DataMember(Name="messages"), Preserve] public List _messages { get; set; } /// [IgnoreDataMember] public IEnumerable Notifications => _notifications ?? new List(0); [DataMember(Name="notifications"), Preserve] public List _notifications { get; set; } /// [IgnoreDataMember] public IEnumerable Objects => _objects ?? new List(0); [DataMember(Name="objects"), Preserve] public List _objects { get; set; } /// [IgnoreDataMember] public IEnumerable WalletLedgers => _walletLedgers ?? new List(0); [DataMember(Name="wallet_ledgers"), Preserve] public List _walletLedgers { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Account: ", Account, ", "); output = string.Concat(output, "Friends: [", string.Join(", ", Friends), "], "); output = string.Concat(output, "Groups: [", string.Join(", ", Groups), "], "); output = string.Concat(output, "LeaderboardRecords: [", string.Join(", ", LeaderboardRecords), "], "); output = string.Concat(output, "Messages: [", string.Join(", ", Messages), "], "); output = string.Concat(output, "Notifications: [", string.Join(", ", Notifications), "], "); output = string.Concat(output, "Objects: [", string.Join(", ", Objects), "], "); output = string.Concat(output, "WalletLedgers: [", string.Join(", ", WalletLedgers), "], "); return output; } } /// /// A list of users. /// public interface IConsoleAccountList { /// /// Next cursor. /// string NextCursor { get; } /// /// Approximate total number of users. /// int TotalCount { get; } /// /// A list of users. /// IEnumerable Users { get; } } /// internal class ConsoleAccountList : IConsoleAccountList { /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [DataMember(Name="total_count"), Preserve] public int TotalCount { get; set; } /// [IgnoreDataMember] public IEnumerable Users => _users ?? new List(0); [DataMember(Name="users"), Preserve] public List _users { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "TotalCount: ", TotalCount, ", "); output = string.Concat(output, "Users: [", string.Join(", ", Users), "], "); return output; } } /// /// Add a new console user /// public interface IConsoleAddUserRequest { /// /// Email address of the user. /// string Email { get; } /// /// Require MFA /// bool MfaRequired { get; } /// /// Subscribe to newsletters /// bool NewsletterSubscription { get; } /// /// The password of the user. /// string Password { get; } /// /// Role of this user; /// ConsoleUserRole Role { get; } /// /// The username of the user. /// string Username { get; } } /// internal class ConsoleAddUserRequest : IConsoleAddUserRequest { /// [DataMember(Name="email"), Preserve] public string Email { get; set; } /// [DataMember(Name="mfa_required"), Preserve] public bool MfaRequired { get; set; } /// [DataMember(Name="newsletter_subscription"), Preserve] public bool NewsletterSubscription { get; set; } /// [DataMember(Name="password"), Preserve] public string Password { get; set; } /// [IgnoreDataMember] public ConsoleUserRole Role => _role; [DataMember(Name="role"), Preserve] public ConsoleUserRole _role { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Email: ", Email, ", "); output = string.Concat(output, "MfaRequired: ", MfaRequired, ", "); output = string.Concat(output, "NewsletterSubscription: ", NewsletterSubscription, ", "); output = string.Concat(output, "Password: ", Password, ", "); output = string.Concat(output, "Role: ", Role, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// API Explorer List of Endpoints response message /// public interface IConsoleApiEndpointDescriptor { /// /// /// string BodyTemplate { get; } /// /// /// string Method { get; } } /// internal class ConsoleApiEndpointDescriptor : IConsoleApiEndpointDescriptor { /// [DataMember(Name="body_template"), Preserve] public string BodyTemplate { get; set; } /// [DataMember(Name="method"), Preserve] public string Method { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "BodyTemplate: ", BodyTemplate, ", "); output = string.Concat(output, "Method: ", Method, ", "); return output; } } /// /// API Explorer List of Endpoints /// public interface IConsoleApiEndpointList { /// /// /// IEnumerable Endpoints { get; } /// /// /// IEnumerable RpcEndpoints { get; } } /// internal class ConsoleApiEndpointList : IConsoleApiEndpointList { /// [IgnoreDataMember] public IEnumerable Endpoints => _endpoints ?? new List(0); [DataMember(Name="endpoints"), Preserve] public List _endpoints { get; set; } /// [IgnoreDataMember] public IEnumerable RpcEndpoints => _rpcEndpoints ?? new List(0); [DataMember(Name="rpc_endpoints"), Preserve] public List _rpcEndpoints { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Endpoints: [", string.Join(", ", Endpoints), "], "); output = string.Concat(output, "RpcEndpoints: [", string.Join(", ", RpcEndpoints), "], "); return output; } } /// /// Log out a session and invalidate a session token. /// public interface IConsoleAuthenticateLogoutRequest { /// /// Session token to log out. /// string Token { get; } } /// internal class ConsoleAuthenticateLogoutRequest : IConsoleAuthenticateLogoutRequest { /// [DataMember(Name="token"), Preserve] public string Token { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Token: ", Token, ", "); return output; } } /// /// Request to change MFA. /// public interface IConsoleAuthenticateMFASetupRequest { /// /// MFA code. /// string Code { get; } /// /// MFA token. /// string Mfa { get; } } /// internal class ConsoleAuthenticateMFASetupRequest : IConsoleAuthenticateMFASetupRequest { /// [DataMember(Name="code"), Preserve] public string Code { get; set; } /// [DataMember(Name="mfa"), Preserve] public string Mfa { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Code: ", Code, ", "); output = string.Concat(output, "Mfa: ", Mfa, ", "); return output; } } /// /// Response to change MFA. /// public interface IConsoleAuthenticateMFASetupResponse { /// /// An one-time code to configure the MFA mechanism /// List RecoveryCodes { get; } } /// internal class ConsoleAuthenticateMFASetupResponse : IConsoleAuthenticateMFASetupResponse { /// [DataMember(Name="recovery_codes"), Preserve] public List RecoveryCodes { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "RecoveryCodes: [", string.Join(", ", RecoveryCodes), "], "); return output; } } /// /// Authenticate a console user with username and password. /// public interface IConsoleAuthenticateRequest { /// /// Multi-factor authentication code. /// string Mfa { get; } /// /// The password of the user. /// string Password { get; } /// /// The username of the user. /// string Username { get; } } /// internal class ConsoleAuthenticateRequest : IConsoleAuthenticateRequest { /// [DataMember(Name="mfa"), Preserve] public string Mfa { get; set; } /// [DataMember(Name="password"), Preserve] public string Password { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Mfa: ", Mfa, ", "); output = string.Concat(output, "Password: ", Password, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// API Explorer response definition for CallApiEndpoint /// public interface IConsoleCallApiEndpointResponse { /// /// /// string Body { get; } /// /// /// string ErrorMessage { get; } } /// internal class ConsoleCallApiEndpointResponse : IConsoleCallApiEndpointResponse { /// [DataMember(Name="body"), Preserve] public string Body { get; set; } /// [DataMember(Name="error_message"), Preserve] public string ErrorMessage { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Body: ", Body, ", "); output = string.Concat(output, "ErrorMessage: ", ErrorMessage, ", "); return output; } } /// /// The current server configuration and any associated warnings. /// public interface IConsoleConfig { /// /// JSON-encoded active server configuration. /// string Config { get; } /// /// Server version /// string ServerVersion { get; } /// /// Any warnings about the current config. /// IEnumerable Warnings { get; } } /// internal class ConsoleConfig : IConsoleConfig { /// [DataMember(Name="config"), Preserve] public string Config { get; set; } /// [DataMember(Name="server_version"), Preserve] public string ServerVersion { get; set; } /// [IgnoreDataMember] public IEnumerable Warnings => _warnings ?? new List(0); [DataMember(Name="warnings"), Preserve] public List _warnings { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Config: ", Config, ", "); output = string.Concat(output, "ServerVersion: ", ServerVersion, ", "); output = string.Concat(output, "Warnings: [", string.Join(", ", Warnings), "], "); return output; } } /// /// A console user session. /// public interface IConsoleConsoleSession { /// /// MFA code required to setup the MFA mechanism. /// string MfaCode { get; } /// /// A session token (JWT) for the console user. /// string Token { get; } } /// internal class ConsoleConsoleSession : IConsoleConsoleSession { /// [DataMember(Name="mfa_code"), Preserve] public string MfaCode { get; set; } /// [DataMember(Name="token"), Preserve] public string Token { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "MfaCode: ", MfaCode, ", "); output = string.Concat(output, "Token: ", Token, ", "); return output; } } /// /// /// public interface IConsoleDeleteChannelMessagesResponse { /// /// Total number of messages deleted. /// string Total { get; } } /// internal class ConsoleDeleteChannelMessagesResponse : IConsoleDeleteChannelMessagesResponse { /// [DataMember(Name="total"), Preserve] public string Total { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Total: ", Total, ", "); return output; } } /// /// An export of all information stored for a group. /// public interface IConsoleGroupExport { /// /// The group details. /// IApiGroup Group { get; } /// /// The group's list of members. /// IEnumerable Members { get; } } /// internal class ConsoleGroupExport : IConsoleGroupExport { /// [IgnoreDataMember] public IApiGroup Group => _group; [DataMember(Name="group"), Preserve] public ApiGroup _group { get; set; } /// [IgnoreDataMember] public IEnumerable Members => _members ?? new List(0); [DataMember(Name="members"), Preserve] public List _members { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Group: ", Group, ", "); output = string.Concat(output, "Members: [", string.Join(", ", Members), "], "); return output; } } /// /// /// public enum ConsoleListChannelMessagesRequestType { /// /// /// UNKNOWN = 0, /// /// /// ROOM = 1, /// /// /// GROUP = 2, /// /// /// DIRECT = 3, } /// /// /// public interface IConsoleMatchListMatch { /// /// The API match /// INakamaapiMatch ApiMatch { get; } /// /// The node name /// string Node { get; } } /// internal class ConsoleMatchListMatch : IConsoleMatchListMatch { /// [IgnoreDataMember] public INakamaapiMatch ApiMatch => _apiMatch; [DataMember(Name="api_match"), Preserve] public NakamaapiMatch _apiMatch { get; set; } /// [DataMember(Name="node"), Preserve] public string Node { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ApiMatch: ", ApiMatch, ", "); output = string.Concat(output, "Node: ", Node, ", "); return output; } } /// /// Match state /// public interface IConsoleMatchState { /// /// Presence list. /// IEnumerable Presences { get; } /// /// State. /// string State { get; } /// /// Current tick number. /// string Tick { get; } } /// internal class ConsoleMatchState : IConsoleMatchState { /// [IgnoreDataMember] public IEnumerable Presences => _presences ?? new List(0); [DataMember(Name="presences"), Preserve] public List _presences { get; set; } /// [DataMember(Name="state"), Preserve] public string State { get; set; } /// [DataMember(Name="tick"), Preserve] public string Tick { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Presences: [", string.Join(", ", Presences), "], "); output = string.Concat(output, "State: ", State, ", "); output = string.Concat(output, "Tick: ", Tick, ", "); return output; } } /// /// Runtime information /// public interface IConsoleRuntimeInfo { /// /// Go loaded modules /// IEnumerable GoModules { get; } /// /// Go registered RPC functions /// List GoRpcFunctions { get; } /// /// JavaScript loaded modules /// IEnumerable JsModules { get; } /// /// JavaScript registered RPC functions /// List JsRpcFunctions { get; } /// /// Lua loaded modules /// IEnumerable LuaModules { get; } /// /// Lua registered RPC functions /// List LuaRpcFunctions { get; } } /// internal class ConsoleRuntimeInfo : IConsoleRuntimeInfo { /// [IgnoreDataMember] public IEnumerable GoModules => _goModules ?? new List(0); [DataMember(Name="go_modules"), Preserve] public List _goModules { get; set; } /// [DataMember(Name="go_rpc_functions"), Preserve] public List GoRpcFunctions { get; set; } /// [IgnoreDataMember] public IEnumerable JsModules => _jsModules ?? new List(0); [DataMember(Name="js_modules"), Preserve] public List _jsModules { get; set; } /// [DataMember(Name="js_rpc_functions"), Preserve] public List JsRpcFunctions { get; set; } /// [IgnoreDataMember] public IEnumerable LuaModules => _luaModules ?? new List(0); [DataMember(Name="lua_modules"), Preserve] public List _luaModules { get; set; } /// [DataMember(Name="lua_rpc_functions"), Preserve] public List LuaRpcFunctions { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "GoModules: [", string.Join(", ", GoModules), "], "); output = string.Concat(output, "GoRpcFunctions: [", string.Join(", ", GoRpcFunctions), "], "); output = string.Concat(output, "JsModules: [", string.Join(", ", JsModules), "], "); output = string.Concat(output, "JsRpcFunctions: [", string.Join(", ", JsRpcFunctions), "], "); output = string.Concat(output, "LuaModules: [", string.Join(", ", LuaModules), "], "); output = string.Concat(output, "LuaRpcFunctions: [", string.Join(", ", LuaRpcFunctions), "], "); return output; } } /// /// A single setting. /// public interface IConsoleSetting { /// /// Name identifier. /// string Name { get; } /// /// Update time. /// string UpdateTimeSec { get; } /// /// Setting value. /// string Value { get; } } /// internal class ConsoleSetting : IConsoleSetting { /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="update_time_sec"), Preserve] public string UpdateTimeSec { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "UpdateTimeSec: ", UpdateTimeSec, ", "); output = string.Concat(output, "Value: ", Value, ", "); return output; } } /// /// A list of settings. /// public interface IConsoleSettingList { /// /// A list of settings. /// IEnumerable Settings { get; } } /// internal class ConsoleSettingList : IConsoleSettingList { /// [IgnoreDataMember] public IEnumerable Settings => _settings ?? new List(0); [DataMember(Name="settings"), Preserve] public List _settings { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Settings: [", string.Join(", ", Settings), "], "); return output; } } /// /// /// public enum ConsoleStatusHealth { /// /// /// STATUS_HEALTH_OK = 0, /// /// /// STATUS_HEALTH_ERROR = 1, /// /// /// STATUS_HEALTH_CONNECTING = 2, /// /// /// STATUS_HEALTH_DISCONNECTING = 3, } /// /// List of nodes and their stats. /// public interface IConsoleStatusList { /// /// List of nodes and their stats. /// IEnumerable Nodes { get; } /// /// Timestamp /// string Timestamp { get; } } /// internal class ConsoleStatusList : IConsoleStatusList { /// [IgnoreDataMember] public IEnumerable Nodes => _nodes ?? new List(0); [DataMember(Name="nodes"), Preserve] public List _nodes { get; set; } /// [DataMember(Name="timestamp"), Preserve] public string Timestamp { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Nodes: [", string.Join(", ", Nodes), "], "); output = string.Concat(output, "Timestamp: ", Timestamp, ", "); return output; } } /// /// The status of a Nakama node. /// public interface IConsoleStatusListStatus { /// /// Average input bandwidth usage. /// double AvgInputKbs { get; } /// /// Average response latency in milliseconds. /// double AvgLatencyMs { get; } /// /// Average output bandwidth usage. /// double AvgOutputKbs { get; } /// /// Average number of requests per second. /// double AvgRateSec { get; } /// /// Current number of running goroutines. /// int GoroutineCount { get; } /// /// Health score. /// ConsoleStatusHealth Health { get; } /// /// Current number of active authoritative matches. /// int MatchCount { get; } /// /// Node name. /// string Name { get; } /// /// Currently registered live presences. /// int PresenceCount { get; } /// /// Currently connected sessions. /// int SessionCount { get; } } /// internal class ConsoleStatusListStatus : IConsoleStatusListStatus { /// [DataMember(Name="avg_input_kbs"), Preserve] public double AvgInputKbs { get; set; } /// [DataMember(Name="avg_latency_ms"), Preserve] public double AvgLatencyMs { get; set; } /// [DataMember(Name="avg_output_kbs"), Preserve] public double AvgOutputKbs { get; set; } /// [DataMember(Name="avg_rate_sec"), Preserve] public double AvgRateSec { get; set; } /// [DataMember(Name="goroutine_count"), Preserve] public int GoroutineCount { get; set; } /// [IgnoreDataMember] public ConsoleStatusHealth Health => _health; [DataMember(Name="health"), Preserve] public ConsoleStatusHealth _health { get; set; } /// [DataMember(Name="match_count"), Preserve] public int MatchCount { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="presence_count"), Preserve] public int PresenceCount { get; set; } /// [DataMember(Name="session_count"), Preserve] public int SessionCount { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AvgInputKbs: ", AvgInputKbs, ", "); output = string.Concat(output, "AvgLatencyMs: ", AvgLatencyMs, ", "); output = string.Concat(output, "AvgOutputKbs: ", AvgOutputKbs, ", "); output = string.Concat(output, "AvgRateSec: ", AvgRateSec, ", "); output = string.Concat(output, "GoroutineCount: ", GoroutineCount, ", "); output = string.Concat(output, "Health: ", Health, ", "); output = string.Concat(output, "MatchCount: ", MatchCount, ", "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "PresenceCount: ", PresenceCount, ", "); output = string.Concat(output, "SessionCount: ", SessionCount, ", "); return output; } } /// /// /// public interface IConsoleStorageCollectionsList { /// /// Available collection names in the whole of the storage /// List Collections { get; } } /// internal class ConsoleStorageCollectionsList : IConsoleStorageCollectionsList { /// [DataMember(Name="collections"), Preserve] public List Collections { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Collections: [", string.Join(", ", Collections), "], "); return output; } } /// /// List of storage objects. /// public interface IConsoleStorageList { /// /// Next page cursor if any /// string NextCursor { get; } /// /// List of storage objects matching list/filter operation. /// IEnumerable Objects { get; } /// /// Approximate total number of storage objects. /// int TotalCount { get; } } /// internal class ConsoleStorageList : IConsoleStorageList { /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [IgnoreDataMember] public IEnumerable Objects => _objects ?? new List(0); [DataMember(Name="objects"), Preserve] public List _objects { get; set; } /// [DataMember(Name="total_count"), Preserve] public int TotalCount { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "Objects: [", string.Join(", ", Objects), "], "); output = string.Concat(output, "TotalCount: ", TotalCount, ", "); return output; } } /// /// An object within the storage engine. /// public interface IConsoleStorageListObject { /// /// The collection which stores the object. /// string Collection { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was created. /// string CreateTime { get; } /// /// The key of the object within the collection. /// string Key { get; } /// /// The read access permissions for the object. /// int PermissionRead { get; } /// /// The write access permissions for the object. /// int PermissionWrite { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the object was last updated. /// string UpdateTime { get; } /// /// The user owner of the object. /// string UserId { get; } /// /// The version hash of the object. /// string Version { get; } } /// internal class ConsoleStorageListObject : IConsoleStorageListObject { /// [DataMember(Name="collection"), Preserve] public string Collection { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="key"), Preserve] public string Key { get; set; } /// [DataMember(Name="permission_read"), Preserve] public int PermissionRead { get; set; } /// [DataMember(Name="permission_write"), Preserve] public int PermissionWrite { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } /// [DataMember(Name="version"), Preserve] public string Version { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Collection: ", Collection, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Key: ", Key, ", "); output = string.Concat(output, "PermissionRead: ", PermissionRead, ", "); output = string.Concat(output, "PermissionWrite: ", PermissionWrite, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); output = string.Concat(output, "Version: ", Version, ", "); return output; } } /// /// A list of console users. /// public interface IConsoleUserList { /// /// A list of users. /// IEnumerable Users { get; } } /// internal class ConsoleUserList : IConsoleUserList { /// [IgnoreDataMember] public IEnumerable Users => _users ?? new List(0); [DataMember(Name="users"), Preserve] public List _users { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Users: [", string.Join(", ", Users), "], "); return output; } } /// /// A console user /// public interface IConsoleUserListUser { /// /// Email of the user /// string Email { get; } /// /// Whether the user has MFA enabled. /// bool MfaEnabled { get; } /// /// Whether the user is required to setup MFA. /// bool MfaRequired { get; } /// /// Role of the user; /// ConsoleUserRole Role { get; } /// /// Username of the user /// string Username { get; } } /// internal class ConsoleUserListUser : IConsoleUserListUser { /// [DataMember(Name="email"), Preserve] public string Email { get; set; } /// [DataMember(Name="mfa_enabled"), Preserve] public bool MfaEnabled { get; set; } /// [DataMember(Name="mfa_required"), Preserve] public bool MfaRequired { get; set; } /// [IgnoreDataMember] public ConsoleUserRole Role => _role; [DataMember(Name="role"), Preserve] public ConsoleUserRole _role { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Email: ", Email, ", "); output = string.Concat(output, "MfaEnabled: ", MfaEnabled, ", "); output = string.Concat(output, "MfaRequired: ", MfaRequired, ", "); output = string.Concat(output, "Role: ", Role, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// - USER_ROLE_ADMIN: All access /// - USER_ROLE_DEVELOPER: Best for developers, also enables APIs and API explorer /// - USER_ROLE_MAINTAINER: Best for users who regularly update player information. /// - USER_ROLE_READONLY: Read-only role for those only need to view data /// public enum ConsoleUserRole { /// /// /// USER_ROLE_UNKNOWN = 0, /// /// /// USER_ROLE_ADMIN = 1, /// /// /// USER_ROLE_DEVELOPER = 2, /// /// /// USER_ROLE_MAINTAINER = 3, /// /// /// USER_ROLE_READONLY = 4, } /// /// An individual update to a user's wallet. /// public interface IConsoleWalletLedger { /// /// The changeset. /// string Changeset { get; } /// /// The UNIX time when the wallet ledger item was created. /// string CreateTime { get; } /// /// The identifier of this wallet change. /// string Id { get; } /// /// Any associated metadata. /// string Metadata { get; } /// /// The UNIX time when the wallet ledger item was updated. /// string UpdateTime { get; } /// /// The user ID this wallet ledger item belongs to. /// string UserId { get; } } /// internal class ConsoleWalletLedger : IConsoleWalletLedger { /// [DataMember(Name="changeset"), Preserve] public string Changeset { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Changeset: ", Changeset, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); return output; } } /// /// List of wallet ledger items for a particular user. /// public interface IConsoleWalletLedgerList { /// /// A list of wallet ledger items. /// IEnumerable Items { get; } /// /// The cursor to send when retrieving the next older page, if any. /// string NextCursor { get; } /// /// The cursor to send when retrieving the previous page newer, if any. /// string PrevCursor { get; } } /// internal class ConsoleWalletLedgerList : IConsoleWalletLedgerList { /// [IgnoreDataMember] public IEnumerable Items => _items ?? new List(0); [DataMember(Name="items"), Preserve] public List _items { get; set; } /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Items: [", string.Join(", ", Items), "], "); output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); return output; } } /// /// /// public interface IGooglerpcStatus { /// /// /// int Code { get; } /// /// /// IEnumerable Details { get; } /// /// /// string Message { get; } } /// internal class GooglerpcStatus : IGooglerpcStatus { /// [DataMember(Name="code"), Preserve] public int Code { get; set; } /// [IgnoreDataMember] public IEnumerable Details => _details ?? new List(0); [DataMember(Name="details"), Preserve] public List _details { get; set; } /// [DataMember(Name="message"), Preserve] public string Message { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Code: ", Code, ", "); output = string.Concat(output, "Details: [", string.Join(", ", Details), "], "); output = string.Concat(output, "Message: ", Message, ", "); return output; } } /// /// A user with additional account details. Always the current user. /// public interface INakamaapiAccount { /// /// The custom id in the user's account. /// string CustomId { get; } /// /// The devices which belong to the user's account. /// IEnumerable Devices { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user's account was disabled/banned. /// string DisableTime { get; } /// /// The email address of the user. /// string Email { get; } /// /// The user object. /// INakamaapiUser User { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user's email was verified. /// string VerifyTime { get; } /// /// The user's wallet data. /// string Wallet { get; } } /// internal class NakamaapiAccount : INakamaapiAccount { /// [DataMember(Name="custom_id"), Preserve] public string CustomId { get; set; } /// [IgnoreDataMember] public IEnumerable Devices => _devices ?? new List(0); [DataMember(Name="devices"), Preserve] public List _devices { get; set; } /// [DataMember(Name="disable_time"), Preserve] public string DisableTime { get; set; } /// [DataMember(Name="email"), Preserve] public string Email { get; set; } /// [IgnoreDataMember] public INakamaapiUser User => _user; [DataMember(Name="user"), Preserve] public NakamaapiUser _user { get; set; } /// [DataMember(Name="verify_time"), Preserve] public string VerifyTime { get; set; } /// [DataMember(Name="wallet"), Preserve] public string Wallet { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CustomId: ", CustomId, ", "); output = string.Concat(output, "Devices: [", string.Join(", ", Devices), "], "); output = string.Concat(output, "DisableTime: ", DisableTime, ", "); output = string.Concat(output, "Email: ", Email, ", "); output = string.Concat(output, "User: ", User, ", "); output = string.Concat(output, "VerifyTime: ", VerifyTime, ", "); output = string.Concat(output, "Wallet: ", Wallet, ", "); return output; } } /// /// Represents a realtime match. /// public interface INakamaapiMatch { /// /// True if it's an server-managed authoritative match, false otherwise. /// bool Authoritative { get; } /// /// Handler name /// string HandlerName { get; } /// /// Match label, if any. /// string Label { get; } /// /// The ID of the match, can be used to join. /// string MatchId { get; } /// /// Current number of users in the match. /// int Size { get; } /// /// Tick Rate /// int TickRate { get; } } /// internal class NakamaapiMatch : INakamaapiMatch { /// [DataMember(Name="authoritative"), Preserve] public bool Authoritative { get; set; } /// [DataMember(Name="handler_name"), Preserve] public string HandlerName { get; set; } /// [DataMember(Name="label"), Preserve] public string Label { get; set; } /// [DataMember(Name="match_id"), Preserve] public string MatchId { get; set; } /// [DataMember(Name="size"), Preserve] public int Size { get; set; } /// [DataMember(Name="tick_rate"), Preserve] public int TickRate { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Authoritative: ", Authoritative, ", "); output = string.Concat(output, "HandlerName: ", HandlerName, ", "); output = string.Concat(output, "Label: ", Label, ", "); output = string.Concat(output, "MatchId: ", MatchId, ", "); output = string.Concat(output, "Size: ", Size, ", "); output = string.Concat(output, "TickRate: ", TickRate, ", "); return output; } } /// /// A notification in the server. /// public interface INakamaapiNotification { /// /// Category code for this notification. /// int Code { get; } /// /// Content of the notification in JSON. /// string Content { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the notification was created. /// string CreateTime { get; } /// /// ID of the Notification. /// string Id { get; } /// /// True if this notification was persisted to the database. /// bool Persistent { get; } /// /// ID of the sender, if a user. Otherwise 'null'. /// string SenderId { get; } /// /// Subject of the notification. /// string Subject { get; } } /// internal class NakamaapiNotification : INakamaapiNotification { /// [DataMember(Name="code"), Preserve] public int Code { get; set; } /// [DataMember(Name="content"), Preserve] public string Content { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="persistent"), Preserve] public bool Persistent { get; set; } /// [DataMember(Name="sender_id"), Preserve] public string SenderId { get; set; } /// [DataMember(Name="subject"), Preserve] public string Subject { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Code: ", Code, ", "); output = string.Concat(output, "Content: ", Content, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "Persistent: ", Persistent, ", "); output = string.Concat(output, "SenderId: ", SenderId, ", "); output = string.Concat(output, "Subject: ", Subject, ", "); return output; } } /// /// A user in the server. /// public interface INakamaapiUser { /// /// The Apple Sign In ID in the user's account. /// string AppleId { get; } /// /// A URL for an avatar image. /// string AvatarUrl { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user was created. /// string CreateTime { get; } /// /// The display name of the user. /// string DisplayName { get; } /// /// Number of related edges to this user. /// int EdgeCount { get; } /// /// The Facebook id in the user's account. /// string FacebookId { get; } /// /// The Facebook Instant Game ID in the user's account. /// string FacebookInstantGameId { get; } /// /// The Apple Game Center in of the user's account. /// string GamecenterId { get; } /// /// The Google id in the user's account. /// string GoogleId { get; } /// /// The id of the user's account. /// string Id { get; } /// /// The language expected to be a tag which follows the BCP-47 spec. /// string LangTag { get; } /// /// The location set by the user. /// string Location { get; } /// /// Additional information stored as a JSON object. /// string Metadata { get; } /// /// Indicates whether the user is currently online. /// bool Online { get; } /// /// The Steam id in the user's account. /// string SteamId { get; } /// /// The timezone set by the user. /// string Timezone { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the user was last updated. /// string UpdateTime { get; } /// /// The username of the user's account. /// string Username { get; } } /// internal class NakamaapiUser : INakamaapiUser { /// [DataMember(Name="apple_id"), Preserve] public string AppleId { get; set; } /// [DataMember(Name="avatar_url"), Preserve] public string AvatarUrl { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="display_name"), Preserve] public string DisplayName { get; set; } /// [DataMember(Name="edge_count"), Preserve] public int EdgeCount { get; set; } /// [DataMember(Name="facebook_id"), Preserve] public string FacebookId { get; set; } /// [DataMember(Name="facebook_instant_game_id"), Preserve] public string FacebookInstantGameId { get; set; } /// [DataMember(Name="gamecenter_id"), Preserve] public string GamecenterId { get; set; } /// [DataMember(Name="google_id"), Preserve] public string GoogleId { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="lang_tag"), Preserve] public string LangTag { get; set; } /// [DataMember(Name="location"), Preserve] public string Location { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="online"), Preserve] public bool Online { get; set; } /// [DataMember(Name="steam_id"), Preserve] public string SteamId { get; set; } /// [DataMember(Name="timezone"), Preserve] public string Timezone { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "AppleId: ", AppleId, ", "); output = string.Concat(output, "AvatarUrl: ", AvatarUrl, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "DisplayName: ", DisplayName, ", "); output = string.Concat(output, "EdgeCount: ", EdgeCount, ", "); output = string.Concat(output, "FacebookId: ", FacebookId, ", "); output = string.Concat(output, "FacebookInstantGameId: ", FacebookInstantGameId, ", "); output = string.Concat(output, "GamecenterId: ", GamecenterId, ", "); output = string.Concat(output, "GoogleId: ", GoogleId, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "LangTag: ", LangTag, ", "); output = string.Concat(output, "Location: ", Location, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "Online: ", Online, ", "); output = string.Concat(output, "SteamId: ", SteamId, ", "); output = string.Concat(output, "Timezone: ", Timezone, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// Account information. /// public interface INakamaconsoleAccount { /// /// The user's account details. /// INakamaapiAccount Account { get; } /// /// The UNIX time when the account was disabled. /// string DisableTime { get; } } /// internal class NakamaconsoleAccount : INakamaconsoleAccount { /// [IgnoreDataMember] public INakamaapiAccount Account => _account; [DataMember(Name="account"), Preserve] public NakamaapiAccount _account { get; set; } /// [DataMember(Name="disable_time"), Preserve] public string DisableTime { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Account: ", Account, ", "); output = string.Concat(output, "DisableTime: ", DisableTime, ", "); return output; } } /// /// A list of groups. /// public interface INakamaconsoleGroupList { /// /// A list of groups. /// IEnumerable Groups { get; } /// /// Next cursor. /// string NextCursor { get; } /// /// Approximate total number of groups. /// int TotalCount { get; } } /// internal class NakamaconsoleGroupList : INakamaconsoleGroupList { /// [IgnoreDataMember] public IEnumerable Groups => _groups ?? new List(0); [DataMember(Name="groups"), Preserve] public List _groups { get; set; } /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [DataMember(Name="total_count"), Preserve] public int TotalCount { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Groups: [", string.Join(", ", Groups), "], "); output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "TotalCount: ", TotalCount, ", "); return output; } } /// /// A leaderboard. /// public interface INakamaconsoleLeaderboard { /// /// Authoritative. /// bool Authoritative { get; } /// /// The category of the leaderboard. e.g. "vip" could be category 1. /// int Category { get; } /// /// The UNIX time when the leaderboard was created. /// string CreateTime { get; } /// /// The description of the leaderboard. May be blank. /// string Description { get; } /// /// Duration of the tournament in seconds. /// int Duration { get; } /// /// The UNIX time when the leaderboard stops being active until next reset. A computed value. /// int EndActive { get; } /// /// The UNIX time when the leaderboard will be stopped. /// string EndTime { get; } /// /// The ID of the leaderboard. /// string Id { get; } /// /// Join required. /// bool JoinRequired { get; } /// /// The maximum score updates allowed per player for the current leaderboard. /// int MaxNumScore { get; } /// /// The maximum number of players for the leaderboard. /// int MaxSize { get; } /// /// Additional information stored as a JSON object. /// string Metadata { get; } /// /// The UNIX time when the tournament is next playable. A computed value. /// int NextReset { get; } /// /// The operator of the leaderboard /// int Operator { get; } /// /// The UNIX time when the tournament was last reset. A computed value. /// int PrevReset { get; } /// /// Reset cron expression. /// string ResetSchedule { get; } /// /// The current number of players in the leaderboard. /// int Size { get; } /// /// ASC or DESC sort mode of scores in the leaderboard. /// int SortOrder { get; } /// /// The UNIX time when the leaderboard start being active. A computed value. /// int StartActive { get; } /// /// The UNIX time when the leaderboard will start. /// string StartTime { get; } /// /// The title for the leaderboard. /// string Title { get; } /// /// Tournament. /// bool Tournament { get; } } /// internal class NakamaconsoleLeaderboard : INakamaconsoleLeaderboard { /// [DataMember(Name="authoritative"), Preserve] public bool Authoritative { get; set; } /// [DataMember(Name="category"), Preserve] public int Category { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="description"), Preserve] public string Description { get; set; } /// [DataMember(Name="duration"), Preserve] public int Duration { get; set; } /// [DataMember(Name="end_active"), Preserve] public int EndActive { get; set; } /// [DataMember(Name="end_time"), Preserve] public string EndTime { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="join_required"), Preserve] public bool JoinRequired { get; set; } /// [DataMember(Name="max_num_score"), Preserve] public int MaxNumScore { get; set; } /// [DataMember(Name="max_size"), Preserve] public int MaxSize { get; set; } /// [DataMember(Name="metadata"), Preserve] public string Metadata { get; set; } /// [DataMember(Name="next_reset"), Preserve] public int NextReset { get; set; } /// [DataMember(Name="operator"), Preserve] public int Operator { get; set; } /// [DataMember(Name="prev_reset"), Preserve] public int PrevReset { get; set; } /// [DataMember(Name="reset_schedule"), Preserve] public string ResetSchedule { get; set; } /// [DataMember(Name="size"), Preserve] public int Size { get; set; } /// [DataMember(Name="sort_order"), Preserve] public int SortOrder { get; set; } /// [DataMember(Name="start_active"), Preserve] public int StartActive { get; set; } /// [DataMember(Name="start_time"), Preserve] public string StartTime { get; set; } /// [DataMember(Name="title"), Preserve] public string Title { get; set; } /// [DataMember(Name="tournament"), Preserve] public bool Tournament { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Authoritative: ", Authoritative, ", "); output = string.Concat(output, "Category: ", Category, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Description: ", Description, ", "); output = string.Concat(output, "Duration: ", Duration, ", "); output = string.Concat(output, "EndActive: ", EndActive, ", "); output = string.Concat(output, "EndTime: ", EndTime, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "JoinRequired: ", JoinRequired, ", "); output = string.Concat(output, "MaxNumScore: ", MaxNumScore, ", "); output = string.Concat(output, "MaxSize: ", MaxSize, ", "); output = string.Concat(output, "Metadata: ", Metadata, ", "); output = string.Concat(output, "NextReset: ", NextReset, ", "); output = string.Concat(output, "Operator: ", Operator, ", "); output = string.Concat(output, "PrevReset: ", PrevReset, ", "); output = string.Concat(output, "ResetSchedule: ", ResetSchedule, ", "); output = string.Concat(output, "Size: ", Size, ", "); output = string.Concat(output, "SortOrder: ", SortOrder, ", "); output = string.Concat(output, "StartActive: ", StartActive, ", "); output = string.Concat(output, "StartTime: ", StartTime, ", "); output = string.Concat(output, "Title: ", Title, ", "); output = string.Concat(output, "Tournament: ", Tournament, ", "); return output; } } /// /// A list of leaderboards. /// public interface INakamaconsoleLeaderboardList { /// /// A cursor, if any. /// string Cursor { get; } /// /// The list of leaderboards returned. /// IEnumerable Leaderboards { get; } /// /// Total count of leaderboards and tournaments. /// int Total { get; } } /// internal class NakamaconsoleLeaderboardList : INakamaconsoleLeaderboardList { /// [DataMember(Name="cursor"), Preserve] public string Cursor { get; set; } /// [IgnoreDataMember] public IEnumerable Leaderboards => _leaderboards ?? new List(0); [DataMember(Name="leaderboards"), Preserve] public List _leaderboards { get; set; } /// [DataMember(Name="total"), Preserve] public int Total { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Cursor: ", Cursor, ", "); output = string.Concat(output, "Leaderboards: [", string.Join(", ", Leaderboards), "], "); output = string.Concat(output, "Total: ", Total, ", "); return output; } } /// /// A list of realtime matches, with their node names. /// public interface INakamaconsoleMatchList { /// /// /// IEnumerable Matches { get; } } /// internal class NakamaconsoleMatchList : INakamaconsoleMatchList { /// [IgnoreDataMember] public IEnumerable Matches => _matches ?? new List(0); [DataMember(Name="matches"), Preserve] public List _matches { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Matches: [", string.Join(", ", Matches), "], "); return output; } } /// /// /// public interface INakamaconsoleNotification { /// /// Category code for this notification. /// int Code { get; } /// /// Content of the notification in JSON. /// string Content { get; } /// /// The UNIX time (for gRPC clients) or ISO string (for REST clients) when the notification was created. /// string CreateTime { get; } /// /// ID of the Notification. /// string Id { get; } /// /// True if this notification was persisted to the database. /// bool Persistent { get; } /// /// ID of the sender, if a user. Otherwise 'null'. /// string SenderId { get; } /// /// Subject of the notification. /// string Subject { get; } /// /// User id. /// string UserId { get; } } /// internal class NakamaconsoleNotification : INakamaconsoleNotification { /// [DataMember(Name="code"), Preserve] public int Code { get; set; } /// [DataMember(Name="content"), Preserve] public string Content { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="persistent"), Preserve] public bool Persistent { get; set; } /// [DataMember(Name="sender_id"), Preserve] public string SenderId { get; set; } /// [DataMember(Name="subject"), Preserve] public string Subject { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Code: ", Code, ", "); output = string.Concat(output, "Content: ", Content, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "Persistent: ", Persistent, ", "); output = string.Concat(output, "SenderId: ", SenderId, ", "); output = string.Concat(output, "Subject: ", Subject, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); return output; } } /// /// /// public interface INakamaconsoleNotificationList { /// /// Next page cursor if any. /// string NextCursor { get; } /// /// List of notifications. /// IEnumerable Notifications { get; } /// /// Previous page cursor if any. /// string PrevCursor { get; } } /// internal class NakamaconsoleNotificationList : INakamaconsoleNotificationList { /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [IgnoreDataMember] public IEnumerable Notifications => _notifications ?? new List(0); [DataMember(Name="notifications"), Preserve] public List _notifications { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "Notifications: [", string.Join(", ", Notifications), "], "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); return output; } } /// /// /// public interface IProtobufAny { /// /// /// string @type { get; } } /// internal class ProtobufAny : IProtobufAny { /// [DataMember(Name="@type"), Preserve] public string @type { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "@type: ", @type, ", "); return output; } } /// /// A user session associated to a stream, usually through a list operation or a join/leave event. /// public interface IRealtimeUserPresence { /// /// Whether this presence generates persistent data/messages, if applicable for the stream type. /// bool Persistence { get; } /// /// A unique session ID identifying the particular connection, because the user may have many. /// string SessionId { get; } /// /// A user-set status message for this stream, if applicable. /// string Status { get; } /// /// The user this presence belongs to. /// string UserId { get; } /// /// The username for display purposes. /// string Username { get; } } /// internal class RealtimeUserPresence : IRealtimeUserPresence { /// [DataMember(Name="persistence"), Preserve] public bool Persistence { get; set; } /// [DataMember(Name="session_id"), Preserve] public string SessionId { get; set; } /// [DataMember(Name="status"), Preserve] public string Status { get; set; } /// [DataMember(Name="user_id"), Preserve] public string UserId { get; set; } /// [DataMember(Name="username"), Preserve] public string Username { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Persistence: ", Persistence, ", "); output = string.Concat(output, "SessionId: ", SessionId, ", "); output = string.Concat(output, "Status: ", Status, ", "); output = string.Concat(output, "UserId: ", UserId, ", "); output = string.Concat(output, "Username: ", Username, ", "); return output; } } /// /// The low level client for the Nakama.Console API. /// internal class ApiClient { public readonly IHttpAdapter HttpAdapter; public int Timeout { get; set; } private readonly Uri _baseUri; public ApiClient(Uri baseUri, IHttpAdapter httpAdapter, int timeout = 10) { _baseUri = baseUri; HttpAdapter = httpAdapter; Timeout = timeout; } /// /// Delete (non-recorded) all user accounts. /// public async Task ConsoleDeleteAccountsAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/console/account"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// List (and optionally filter) accounts. /// public async Task ConsoleListAccountsAsync( string bearerToken, string filter, bool? tombstones, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/console/account"; var queryParams = ""; if (filter != null) { queryParams = string.Concat(queryParams, "filter=", Uri.EscapeDataString(filter), "&"); } if (tombstones != null) { queryParams = string.Concat(queryParams, "tombstones=", tombstones.ToString().ToLower(), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get a list of the user's wallet transactions. /// public async Task ConsoleGetWalletLedgerAsync( string bearerToken, string accountId, int? limit, string cursor, CancellationToken? cancellationToken) { if (accountId == null) { throw new ArgumentException("'accountId' is required but was null."); } var urlpath = "/v2/console/account/{account_id}/wallet"; urlpath = urlpath.Replace("{account_id}", Uri.EscapeDataString(accountId)); var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete all information stored for a user account. /// public async Task ConsoleDeleteAccountAsync( string bearerToken, string id, bool? record_deletion, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; if (record_deletion != null) { queryParams = string.Concat(queryParams, "record_deletion=", record_deletion.ToString().ToLower(), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Get detailed account information for a single user. /// public async Task ConsoleGetAccountAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Update one or more fields on a user account. /// public async Task ConsoleUpdateAccountAsync( string bearerToken, string id, ApiConsole_UpdateAccountRequest body, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/account/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Ban a user. /// public async Task ConsoleBanAccountAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/ban"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Export all information stored about a user account. /// public async Task ConsoleExportAccountAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/export"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get a user's list of friend relationships. /// public async Task ConsoleGetFriendsAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/friend"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete the friend relationship between two users. /// public async Task ConsoleDeleteFriendAsync( string bearerToken, string id, string friendId, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } if (friendId == null) { throw new ArgumentException("'friendId' is required but was null."); } var urlpath = "/v2/console/account/{id}/friend/{friend_id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); urlpath = urlpath.Replace("{friend_id}", Uri.EscapeDataString(friendId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Get a list of groups the user is a member of. /// public async Task ConsoleGetGroupsAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/group"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Remove a user from a group. /// public async Task ConsoleDeleteGroupUserAsync( string bearerToken, string id, string groupId, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } var urlpath = "/v2/console/account/{id}/group/{group_id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); urlpath = urlpath.Replace("{group_id}", Uri.EscapeDataString(groupId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unban a user. /// public async Task ConsoleUnbanAccountAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/unban"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unlink the Apple ID from a user account. /// public async Task ConsoleUnlinkAppleAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/unlink/apple"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unlink the custom ID from a user account. /// public async Task ConsoleUnlinkCustomAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/unlink/custom"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unlink the device ID from a user account. /// public async Task ConsoleUnlinkDeviceAsync( string bearerToken, string id, ApiConsole_UnlinkDeviceRequest body, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/account/{id}/unlink/device"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unlink the email from a user account. /// public async Task ConsoleUnlinkEmailAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/unlink/email"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unlink the Facebook ID from a user account. /// public async Task ConsoleUnlinkFacebookAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/unlink/facebook"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unlink the Facebook Instant Game ID from a user account. /// public async Task ConsoleUnlinkFacebookInstantGameAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/unlink/facebookinstantgame"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unlink the Game Center ID from a user account. /// public async Task ConsoleUnlinkGameCenterAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/unlink/gamecenter"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unlink the Google ID from a user account. /// public async Task ConsoleUnlinkGoogleAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/unlink/google"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Unlink the Steam ID from a user account. /// public async Task ConsoleUnlinkSteamAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/account/{id}/unlink/steam"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Delete a wallet ledger item. /// public async Task ConsoleDeleteWalletLedgerAsync( string bearerToken, string id, string walletId, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } if (walletId == null) { throw new ArgumentException("'walletId' is required but was null."); } var urlpath = "/v2/console/account/{id}/wallet/{wallet_id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); urlpath = urlpath.Replace("{wallet_id}", Uri.EscapeDataString(walletId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Deletes all data /// public async Task ConsoleDeleteAllDataAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/console/all"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// API Explorer - list all endpoints /// public async Task ConsoleListApiEndpointsAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/console/api/endpoints"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// API Explorer - call a custom RPC endpoint /// public async Task ConsoleCallRpcEndpointAsync( string bearerToken, string method, ApiConsole_CallRpcEndpointRequest body, CancellationToken? cancellationToken) { if (method == null) { throw new ArgumentException("'method' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/api/endpoints/rpc/{method}"; urlpath = urlpath.Replace("{method}", Uri.EscapeDataString(method)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// API Explorer - call an endpoint /// public async Task ConsoleCallApiEndpointAsync( string bearerToken, string method, ApiConsole_CallApiEndpointRequest body, CancellationToken? cancellationToken) { if (method == null) { throw new ArgumentException("'method' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/api/endpoints/{method}"; urlpath = urlpath.Replace("{method}", Uri.EscapeDataString(method)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Authenticate a console user with username and password. /// public async Task ConsoleAuthenticateAsync( ConsoleAuthenticateRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/authenticate"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Log out a session and invalidate the session token. /// public async Task ConsoleAuthenticateLogoutAsync( string bearerToken, ConsoleAuthenticateLogoutRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/authenticate/logout"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Change an account's MFA using a code, usually delivered over email. /// public async Task ConsoleAuthenticateMFASetupAsync( string bearerToken, ConsoleAuthenticateMFASetupRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/authenticate/mfa"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List channel messages with the selected filter /// public async Task ConsoleListChannelMessagesAsync( string bearerToken, string type, string label, string groupId, string userIdOne, string userIdTwo, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/console/channel"; var queryParams = ""; if (type != null) { queryParams = string.Concat(queryParams, "type=", Uri.EscapeDataString(type), "&"); } if (label != null) { queryParams = string.Concat(queryParams, "label=", Uri.EscapeDataString(label), "&"); } if (groupId != null) { queryParams = string.Concat(queryParams, "group_id=", Uri.EscapeDataString(groupId), "&"); } if (userIdOne != null) { queryParams = string.Concat(queryParams, "user_id_one=", Uri.EscapeDataString(userIdOne), "&"); } if (userIdTwo != null) { queryParams = string.Concat(queryParams, "user_id_two=", Uri.EscapeDataString(userIdTwo), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get server config and configuration warnings. /// public async Task ConsoleGetConfigAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/console/config"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List (and optionally filter) groups. /// public async Task ConsoleListGroupsAsync( string bearerToken, string filter, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/console/group"; var queryParams = ""; if (filter != null) { queryParams = string.Concat(queryParams, "filter=", Uri.EscapeDataString(filter), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Demote a user from a group. /// public async Task ConsoleDemoteGroupMemberAsync( string bearerToken, string groupId, string id, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/group/{group_id}/account/{id}/demote"; urlpath = urlpath.Replace("{group_id}", Uri.EscapeDataString(groupId)); urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Promote a user from a group. /// public async Task ConsolePromoteGroupMemberAsync( string bearerToken, string groupId, string id, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/group/{group_id}/account/{id}/promote"; urlpath = urlpath.Replace("{group_id}", Uri.EscapeDataString(groupId)); urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Add/join members to a group. /// public async Task ConsoleAddGroupUsersAsync( string bearerToken, string groupId, ApiConsole_AddGroupUsersRequest body, CancellationToken? cancellationToken) { if (groupId == null) { throw new ArgumentException("'groupId' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/group/{group_id}/add"; urlpath = urlpath.Replace("{group_id}", Uri.EscapeDataString(groupId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Remove a group. /// public async Task ConsoleDeleteGroupAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/group/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Get detailed group information. /// public async Task ConsoleGetGroupAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/group/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Update one or more fields on a group. /// public async Task ConsoleUpdateGroupAsync( string bearerToken, string id, ApiConsole_UpdateGroupRequest body, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/group/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Export all information stored about a group. /// public async Task ConsoleExportGroupAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/group/{id}/export"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get a list of members of the group. /// public async Task ConsoleGetMembersAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/group/{id}/member"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get purchase by transaction_id /// public async Task ConsoleGetPurchaseAsync( string bearerToken, string transactionId, CancellationToken? cancellationToken) { if (transactionId == null) { throw new ArgumentException("'transactionId' is required but was null."); } var urlpath = "/v2/console/iap/purchase/{transaction_id}"; urlpath = urlpath.Replace("{transaction_id}", Uri.EscapeDataString(transactionId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get subscription by original_transaction_id /// public async Task ConsoleGetSubscriptionAsync( string bearerToken, string originalTransactionId, CancellationToken? cancellationToken) { if (originalTransactionId == null) { throw new ArgumentException("'originalTransactionId' is required but was null."); } var urlpath = "/v2/console/iap/subscription/{original_transaction_id}"; urlpath = urlpath.Replace("{original_transaction_id}", Uri.EscapeDataString(originalTransactionId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List leaderboards /// public async Task ConsoleListLeaderboardsAsync( string bearerToken, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/console/leaderboard"; var queryParams = ""; if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete leaderboard /// public async Task ConsoleDeleteLeaderboardAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/leaderboard/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Get leaderboard. /// public async Task ConsoleGetLeaderboardAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/leaderboard/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete leaderboard record /// public async Task ConsoleDeleteLeaderboardRecordAsync( string bearerToken, string id, string ownerId, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } if (ownerId == null) { throw new ArgumentException("'ownerId' is required but was null."); } var urlpath = "/v2/console/leaderboard/{id}/owner/{owner_id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); urlpath = urlpath.Replace("{owner_id}", Uri.EscapeDataString(ownerId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// List leaderboard records. /// public async Task ConsoleListLeaderboardRecordsAsync( string bearerToken, string leaderboardId, IEnumerable ownerIds, int? limit, string cursor, string expiry, CancellationToken? cancellationToken) { if (leaderboardId == null) { throw new ArgumentException("'leaderboardId' is required but was null."); } var urlpath = "/v2/console/leaderboard/{leaderboard_id}/records"; urlpath = urlpath.Replace("{leaderboard_id}", Uri.EscapeDataString(leaderboardId)); var queryParams = ""; foreach (var elem in ownerIds ?? new string[0]) { queryParams = string.Concat(queryParams, "owner_ids=", Uri.EscapeDataString(elem), "&"); } if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } if (expiry != null) { queryParams = string.Concat(queryParams, "expiry=", Uri.EscapeDataString(expiry), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List ongoing matches /// public async Task ConsoleListMatchesAsync( string bearerToken, int? limit, bool? authoritative, string label, int? min_size, int? max_size, string matchId, string query, string node, CancellationToken? cancellationToken) { var urlpath = "/v2/console/match"; var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (authoritative != null) { queryParams = string.Concat(queryParams, "authoritative=", authoritative.ToString().ToLower(), "&"); } if (label != null) { queryParams = string.Concat(queryParams, "label=", Uri.EscapeDataString(label), "&"); } if (min_size != null) { queryParams = string.Concat(queryParams, "min_size=", min_size, "&"); } if (max_size != null) { queryParams = string.Concat(queryParams, "max_size=", max_size, "&"); } if (matchId != null) { queryParams = string.Concat(queryParams, "match_id=", Uri.EscapeDataString(matchId), "&"); } if (query != null) { queryParams = string.Concat(queryParams, "query=", Uri.EscapeDataString(query), "&"); } if (node != null) { queryParams = string.Concat(queryParams, "node=", Uri.EscapeDataString(node), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get current state of a running match /// public async Task ConsoleGetMatchStateAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/match/{id}/state"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete messages. /// public async Task ConsoleDeleteChannelMessagesAsync( string bearerToken, string before, IEnumerable ids, CancellationToken? cancellationToken) { var urlpath = "/v2/console/message"; var queryParams = ""; if (before != null) { queryParams = string.Concat(queryParams, "before=", Uri.EscapeDataString(before), "&"); } foreach (var elem in ids ?? new string[0]) { queryParams = string.Concat(queryParams, "ids=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List notifications. /// public async Task ConsoleListNotificationsAsync( string bearerToken, string userId, int? limit, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/console/notification"; var queryParams = ""; if (userId != null) { queryParams = string.Concat(queryParams, "user_id=", Uri.EscapeDataString(userId), "&"); } if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete notification /// public async Task ConsoleDeleteNotificationAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/notification/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Get a notification by id. /// public async Task ConsoleGetNotificationAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v2/console/notification/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List validated purchases /// public async Task ConsoleListPurchasesAsync( string bearerToken, string userId, int? limit, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/console/purchase"; var queryParams = ""; if (userId != null) { queryParams = string.Concat(queryParams, "user_id=", Uri.EscapeDataString(userId), "&"); } if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get runtime info /// public async Task ConsoleGetRuntimeAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/console/runtime"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List settings /// public async Task ConsoleListSettingsAsync( string bearerToken, IEnumerable names, CancellationToken? cancellationToken) { var urlpath = "/v2/console/setting"; var queryParams = ""; foreach (var elem in names ?? new string[0]) { queryParams = string.Concat(queryParams, "names=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get console settings. /// public async Task ConsoleGetSettingAsync( string bearerToken, string name, CancellationToken? cancellationToken) { if (name == null) { throw new ArgumentException("'name' is required but was null."); } var urlpath = "/v2/console/setting/{name}"; urlpath = urlpath.Replace("{name}", Uri.EscapeDataString(name)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Update an existing setting. /// public async Task ConsoleUpdateSettingAsync( string bearerToken, string name, ApiConsole_UpdateSettingRequest body, CancellationToken? cancellationToken) { if (name == null) { throw new ArgumentException("'name' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/setting/{name}"; urlpath = urlpath.Replace("{name}", Uri.EscapeDataString(name)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Get current status data for all nodes. /// public async Task ConsoleGetStatusAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/console/status"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete all storage data. /// public async Task ConsoleDeleteStorageAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/console/storage"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// List (and optionally filter) storage data. /// public async Task ConsoleListStorageAsync( string bearerToken, string userId, string key, string collection, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/console/storage"; var queryParams = ""; if (userId != null) { queryParams = string.Concat(queryParams, "user_id=", Uri.EscapeDataString(userId), "&"); } if (key != null) { queryParams = string.Concat(queryParams, "key=", Uri.EscapeDataString(key), "&"); } if (collection != null) { queryParams = string.Concat(queryParams, "collection=", Uri.EscapeDataString(collection), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List storage collections /// public async Task ConsoleListStorageCollectionsAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/console/storage/collections"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete a storage object. /// public async Task ConsoleDeleteStorageObjectAsync( string bearerToken, string collection, string key, string userId, string version, CancellationToken? cancellationToken) { if (collection == null) { throw new ArgumentException("'collection' is required but was null."); } if (key == null) { throw new ArgumentException("'key' is required but was null."); } if (userId == null) { throw new ArgumentException("'userId' is required but was null."); } var urlpath = "/v2/console/storage/{collection}/{key}/{user_id}"; urlpath = urlpath.Replace("{collection}", Uri.EscapeDataString(collection)); urlpath = urlpath.Replace("{key}", Uri.EscapeDataString(key)); urlpath = urlpath.Replace("{user_id}", Uri.EscapeDataString(userId)); var queryParams = ""; if (version != null) { queryParams = string.Concat(queryParams, "version=", Uri.EscapeDataString(version), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Get a storage object. /// public async Task ConsoleGetStorageAsync( string bearerToken, string collection, string key, string userId, CancellationToken? cancellationToken) { if (collection == null) { throw new ArgumentException("'collection' is required but was null."); } if (key == null) { throw new ArgumentException("'key' is required but was null."); } if (userId == null) { throw new ArgumentException("'userId' is required but was null."); } var urlpath = "/v2/console/storage/{collection}/{key}/{user_id}"; urlpath = urlpath.Replace("{collection}", Uri.EscapeDataString(collection)); urlpath = urlpath.Replace("{key}", Uri.EscapeDataString(key)); urlpath = urlpath.Replace("{user_id}", Uri.EscapeDataString(userId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Write a new storage object or replace an existing one. /// public async Task ConsoleWriteStorageObjectAsync( string bearerToken, string collection, string key, string userId, ApiConsole_WriteStorageObjectRequest body, CancellationToken? cancellationToken) { if (collection == null) { throw new ArgumentException("'collection' is required but was null."); } if (key == null) { throw new ArgumentException("'key' is required but was null."); } if (userId == null) { throw new ArgumentException("'userId' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/storage/{collection}/{key}/{user_id}"; urlpath = urlpath.Replace("{collection}", Uri.EscapeDataString(collection)); urlpath = urlpath.Replace("{key}", Uri.EscapeDataString(key)); urlpath = urlpath.Replace("{user_id}", Uri.EscapeDataString(userId)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "PUT"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete a storage object. /// public async Task ConsoleDeleteStorageObject2Async( string bearerToken, string collection, string key, string userId, string version, CancellationToken? cancellationToken) { if (collection == null) { throw new ArgumentException("'collection' is required but was null."); } if (key == null) { throw new ArgumentException("'key' is required but was null."); } if (userId == null) { throw new ArgumentException("'userId' is required but was null."); } if (version == null) { throw new ArgumentException("'version' is required but was null."); } var urlpath = "/v2/console/storage/{collection}/{key}/{user_id}/{version}"; urlpath = urlpath.Replace("{collection}", Uri.EscapeDataString(collection)); urlpath = urlpath.Replace("{key}", Uri.EscapeDataString(key)); urlpath = urlpath.Replace("{user_id}", Uri.EscapeDataString(userId)); urlpath = urlpath.Replace("{version}", Uri.EscapeDataString(version)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// List validated subscriptions /// public async Task ConsoleListSubscriptionsAsync( string bearerToken, string userId, int? limit, string cursor, CancellationToken? cancellationToken) { var urlpath = "/v2/console/subscription"; var queryParams = ""; if (userId != null) { queryParams = string.Concat(queryParams, "user_id=", Uri.EscapeDataString(userId), "&"); } if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete console user. /// public async Task ConsoleDeleteUserAsync( string bearerToken, string username, CancellationToken? cancellationToken) { var urlpath = "/v2/console/user"; var queryParams = ""; if (username != null) { queryParams = string.Concat(queryParams, "username=", Uri.EscapeDataString(username), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// List (and optionally filter) users. /// public async Task ConsoleListUsersAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v2/console/user"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Add a new console user. /// public async Task ConsoleAddUserAsync( string bearerToken, ConsoleAddUserRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/user"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Sets the user's MFA as required or not required. /// public async Task ConsoleRequireUserMfaAsync( string bearerToken, string username, ApiConsole_RequireUserMfaRequest body, CancellationToken? cancellationToken) { if (username == null) { throw new ArgumentException("'username' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v2/console/user/{username}/mfa/require"; urlpath = urlpath.Replace("{username}", Uri.EscapeDataString(username)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Reset a user's multi-factor authentication credentials. /// public async Task ConsoleResetUserMfaAsync( string bearerToken, string username, CancellationToken? cancellationToken) { if (username == null) { throw new ArgumentException("'username' is required but was null."); } var urlpath = "/v2/console/user/{username}/mfa/reset"; urlpath = urlpath.Replace("{username}", Uri.EscapeDataString(username)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } } } ================================================ FILE: Nakama/GZipHttpClientHandler.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.IO.Compression; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace Nakama { internal class GZipHttpClientHandler : DelegatingHandler { public GZipHttpClientHandler(HttpMessageHandler innerHandler) { InnerHandler = innerHandler; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken ct) { if ((request.Method == HttpMethod.Post || request.Method == HttpMethod.Put) && request.Content != null) { request.Content = new GZipContent(request.Content); } return base.SendAsync(request, ct); } } internal class GZipContent : HttpContent { private readonly HttpContent _content; public GZipContent(HttpContent content) { _content = content; // Must copy all pre-existing headers. foreach (var header in content.Headers) { Headers.TryAddWithoutValidation(header.Key, header.Value); } Headers.ContentEncoding.Add("gzip"); } protected override async Task SerializeToStreamAsync(System.IO.Stream stream, TransportContext context) { using (var gzip = new GZipStream(stream, CompressionMode.Compress, true)) { await _content.CopyToAsync(gzip); } } protected override bool TryComputeLength(out long length) { length = -1; return false; } } } ================================================ FILE: Nakama/HttpRequestAdapter.cs ================================================ // Copyright 2019 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Nakama.TinyJson; namespace Nakama { /// /// HTTP Request adapter which uses the .NET HttpClient to send requests. /// /// /// Accept header is always set as 'application/json'. /// public class HttpRequestAdapter : IHttpAdapter { /// public ILogger Logger { get; set; } public TransientExceptionDelegate TransientExceptionDelegate => IsTransientException; private readonly HttpClient _httpClient; public HttpRequestAdapter(HttpClient httpClient) { _httpClient = httpClient; _httpClient.Timeout = TimeSpan.FromSeconds(80); // Provide a global request timeout as a failsafe. } /// public async Task SendAsync(string method, Uri uri, IDictionary headers, byte[] body, int timeout, CancellationToken? userCancelToken) { var request = new HttpRequestMessage { RequestUri = uri, Method = new HttpMethod(method) }; request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); foreach (var kv in headers) { request.Headers.TryAddWithoutValidation(kv.Key, kv.Value); } if (body != null) { request.Content = new ByteArrayContent(body); Logger?.InfoFormat("Send: method='{0}', uri='{1}', body='{2}'", method, uri, System.Text.Encoding.UTF8.GetString(body)); } else { Logger?.InfoFormat("Send: method='{0}', uri='{1}'", method, uri); } using var ctsTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsTimeout.Token, userCancelToken ?? CancellationToken.None); try { using var response = await _httpClient.SendAsync(request, cts.Token).ConfigureAwait(false); var contents = await response.Content.ReadAsStringAsync(); if ((int)response.StatusCode >= 500) { Logger?.ErrorFormat("Received: status={0}, contents='{1}'", response.StatusCode, contents); // TODO think of best way to map HTTP code to GRPC code since we can't rely // on server to process it. Manually adding the mapping to SDK seems brittle. throw new ApiResponseException((int)response.StatusCode, contents, -1); } if (response.IsSuccessStatusCode) { Logger?.InfoFormat("Received: status={0}, contents='{1}'", response.StatusCode, contents); return contents; } Logger?.ErrorFormat("Received: status={0}, contents='{1}'", response.StatusCode, contents); var decoded = contents.FromJson>(); var message = decoded.TryGetValue("message", out var value1) ? value1.ToString() : string.Empty; var grpcCode = decoded.TryGetValue("code", out var value2) ? (int)value2 : -1; var exception = new ApiResponseException((int)response.StatusCode, message, grpcCode); if (decoded.TryGetValue("error", out var value)) { IHttpAdapterUtil.CopyResponseError(this, value, exception); } throw exception; } catch (TaskCanceledException e) when (ctsTimeout.IsCancellationRequested) { Logger?.ErrorFormat("Request timed out: method='{0}', uri='{1}'", method, uri); throw new TimeoutException($"The request timed out after {timeout} seconds.", e); } catch (Exception exception) when (!(exception is ApiResponseException)) { Logger?.ErrorFormat("Request failed: method='{0}', uri='{1}', exception='{2}'", method, uri, exception); throw; } } /// /// A new HTTP adapter with configuration for gzip support in the underlying HTTP client. /// /// /// NOTE Decompression does not work with Mono AOT on Android. /// /// If automatic decompression should be enabled with the HTTP adapter. /// If automatic compression should be enabled with the HTTP adapter. /// A new HTTP adapter. public static IHttpAdapter WithGzip(bool decompression = false, bool compression = false) { var handler = new HttpClientHandler(); if (handler.SupportsAutomaticDecompression && decompression) { handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; } handler.AllowAutoRedirect = true; var client = new HttpClient(compression ? (HttpMessageHandler)new GZipHttpClientHandler(handler) : handler); return new HttpRequestAdapter(client); } public static bool IsTransientException(Exception e) { if (e is ApiResponseException apiException) { switch (apiException.StatusCode) { case 500 : // Internal Server Error often (but not always) indicates a transient issue in Nakama, e.g., DB connectivity. case 502 : // LB returns this to client if server sends corrupt/invalid data to LB, which may be a transient issue. case 503 : // LB returns this to client if LB determines or is told that server is unable to handle forwarded from LB, which may be a transient issue. case 504 : // LB returns this to client if LB cannot communicate with server, which may be a temporary issue. return true; } } return false; } } } ================================================ FILE: Nakama/IChannel.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// A chat channel on the server. /// public interface IChannel { /// /// The server-assigned channel ID. /// string Id { get; } /// /// The presences visible on the chat channel. /// IEnumerable Presences { get; } /// /// The presence of the current user. i.e. Your self. /// IUserPresence Self { get; } /// /// The name of the chat room, or an empty string if this message was not sent through a chat room. /// string RoomName { get; } /// /// The ID of the group, or an empty string if this message was not sent through a group channel. /// string GroupId { get; } /// /// The ID of the first DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdOne { get; } /// /// The ID of the second DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdTwo { get; } } /// internal class Channel : IChannel { [DataMember(Name="id"), Preserve] public string Id { get; set; } public IEnumerable Presences => _presences ?? UserPresence.NoPresences; [DataMember(Name="presences"), Preserve] public List _presences { get; set; } public IUserPresence Self => _self; [DataMember(Name="self"), Preserve] public UserPresence _self { get; set; } [DataMember(Name="room_name"), Preserve] public string RoomName { get; set; } [DataMember(Name="group_id"), Preserve] public string GroupId { get; set; } [DataMember(Name="user_id_one"), Preserve] public string UserIdOne { get; set; } [DataMember(Name="user_id_two"), Preserve] public string UserIdTwo { get; set; } public override bool Equals(object obj) { if (!(obj is Channel item)) { return false; } return Equals(item); } private bool Equals(IChannel other) => string.Equals(Id, other.Id); public override int GetHashCode() => Id != null ? Id.GetHashCode() : 0; public override string ToString() { var presences = string.Join(", ", Presences); return $"Channel(Id='{Id}', Presences=[{presences}], Self={Self}, RoomName='{RoomName}', GroupId='{GroupId}', UserIdOne='{UserIdOne}', UserIdTwo='{UserIdTwo}')"; } } } ================================================ FILE: Nakama/IChannelMessageAck.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// An acknowledgement from the server when a chat message is delivered to a channel. /// public interface IChannelMessageAck { /// /// The server-assigned channel ID. /// string ChannelId { get; } /// /// A user-defined code for the chat message. /// int Code { get; } /// /// The UNIX time when the message was created. /// string CreateTime { get; } /// /// A unique ID for the chat message. /// string MessageId { get; } /// /// True if the chat message has been stored in history. /// bool Persistent { get; } /// /// The UNIX time when the message was updated. /// string UpdateTime { get; } /// /// The username of the sender of the message. /// string Username { get; } /// /// The name of the chat room, or an empty string if this message was not sent through a chat room. /// string RoomName { get; } /// /// The ID of the group, or an empty string if this message was not sent through a group channel. /// string GroupId { get; } /// /// The ID of the first DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdOne { get; } /// /// The ID of the second DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdTwo { get; } } /// internal class ChannelMessageAck : IChannelMessageAck { [DataMember(Name = "channel_id"), Preserve] public string ChannelId { get; set; } [DataMember(Name = "code"), Preserve] public int Code { get; set; } [DataMember(Name = "create_time"), Preserve] public string CreateTime { get; set; } [DataMember(Name = "message_id"), Preserve] public string MessageId { get; set; } [DataMember(Name = "persistent"), Preserve] public bool Persistent { get; set; } [DataMember(Name = "update_time"), Preserve] public string UpdateTime { get; set; } [DataMember(Name = "username"), Preserve] public string Username { get; set; } [DataMember(Name="room_name"), Preserve] public string RoomName { get; set; } [DataMember(Name="group_id"), Preserve] public string GroupId { get; set; } [DataMember(Name="user_id_one"), Preserve] public string UserIdOne { get; set; } [DataMember(Name="user_id_two"), Preserve] public string UserIdTwo { get; set; } public override string ToString() { return $"ChannelMessageAck(ChannelId='{ChannelId}', Code={Code}, CreateTime={CreateTime}, MessageId='{MessageId}', Persistent={Persistent}, UpdateTime={UpdateTime}, Username='{Username}', RoomName='{RoomName}', GroupId='{GroupId}', UserIdOne='{UserIdOne}', UserIdTwo='{UserIdTwo}')"; } } } ================================================ FILE: Nakama/IChannelPresenceEvent.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// A batch of join and leave presences on a chat channel. /// public interface IChannelPresenceEvent { /// /// The unique identifier of the chat channel. /// string ChannelId { get; } /// /// Presences of the users who joined the channel. /// IEnumerable Joins { get; } /// /// Presences of users who left the channel. /// IEnumerable Leaves { get; } /// /// The name of the chat room, or an empty string if this message was not sent through a chat room. /// string RoomName { get; } /// /// The ID of the group, or an empty string if this message was not sent through a group channel. /// string GroupId { get; } /// /// The ID of the first DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdOne { get; } /// /// The ID of the second DM user, or an empty string if this message was not sent through a DM chat. /// string UserIdTwo { get; } } /// internal class ChannelPresenceEvent : IChannelPresenceEvent { [DataMember(Name="channel_id"), Preserve] public string ChannelId { get; set; } public IEnumerable Joins => _joins ?? new List(0); [DataMember(Name="joins"), Preserve] public List _joins { get; set; } public IEnumerable Leaves => _leaves ?? new List(0); [DataMember(Name="leaves"), Preserve] public List _leaves { get; set; } [DataMember(Name="room_name"), Preserve] public string RoomName { get; set; } [DataMember(Name="group_id"), Preserve] public string GroupId { get; set; } [DataMember(Name="user_id_one"), Preserve] public string UserIdOne { get; set; } [DataMember(Name="user_id_two"), Preserve] public string UserIdTwo { get; set; } public override string ToString() { var joins = string.Join(",", Joins); var leaves = string.Join(",", Leaves); return $"ChannelPresenceEvent(ChannelId='{ChannelId}', Joins=[{joins}], Leaves=[{leaves}], RoomName='{RoomName}', GroupId='{GroupId}', UserIdOne='{UserIdOne}', UserIdTwo='{UserIdTwo}')"; } } } ================================================ FILE: Nakama/IClient.cs ================================================ // Copyright 2022 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Nakama { /// /// A client for the API in Nakama server. /// public interface IClient { /// /// True if the session should be refreshed with an active refresh token. /// bool AutoRefreshSession { get; } /// /// The global retry configuration. See . /// RetryConfiguration GlobalRetryConfiguration { get; set; } /// /// The host address of the server. Defaults to "127.0.0.1". /// string Host { get; } /// /// The port number of the server. Defaults to 7350. /// int Port { get; } /// /// The protocol scheme used to connect with the server. Must be either "http" or "https". /// string Scheme { get; } /// /// The key used to authenticate with the server without a session. Defaults to "defaultkey". /// string ServerKey { get; } /// /// Received a new session after the current one has expired. /// /// /// This event will only be sent when SessionRefreshAsync is called which also happens automatically if /// AutoRefreshSession is enabled. /// /// /// event Action ReceivedSessionUpdated; /// /// Set the timeout in seconds on requests sent to the server. /// int Timeout { get; set; } /// /// The logger to use with the client. /// ILogger Logger { get; set; } /// /// Add one or more friends by id or username. /// /// The session of the user. /// The ids of the users to add or invite as friends. /// The usernames of the users to add as friends. /// Optional metadata to add to the friendship edge. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task AddFriendsAsync(ISession session, IEnumerable ids, IEnumerable usernames = null, string metadata = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Add one or more users to the group. /// /// The session of the user. /// The id of the group to add users into. /// The ids of the users to add or invite to the group. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task AddGroupUsersAsync(ISession session, string groupId, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Authenticate a user with an Apple ID against the server. /// /// A username used to create the user. /// The ID token received from Apple to validate. /// Extra information that will be bundled in the session token. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a session object. Task AuthenticateAppleAsync(string token, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Authenticate a user with a custom id. /// /// A custom identifier usually obtained from an external authentication service. /// A username used to create the user. May be null. /// If the user should be created when authenticated. /// Extra information that will be bundled in the session token. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a session object. Task AuthenticateCustomAsync(string id, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Authenticate a user with a device id. /// /// A device identifier usually obtained from a platform API. /// A username used to create the user. May be null. /// If the user should be created when authenticated. /// Extra information that will be bundled in the session token. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a session object. Task AuthenticateDeviceAsync(string id, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Authenticate a user with an email and password. /// /// The email address of the user. /// The password for the user. /// A username used to create the user. May be null. /// If the user should be created when authenticated. /// Extra information that will be bundled in the session token. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a session object. Task AuthenticateEmailAsync(string email, string password, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Authenticate a user with a Facebook auth token. /// /// An OAuth access token from the Facebook SDK. /// A username used to create the user. May be null. /// If the user should be created when authenticated. /// If the Facebook friends should be imported. /// Extra information that will be bundled in the session token. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a session object. Task AuthenticateFacebookAsync(string token, string username = null, bool create = true, bool import = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Authenticate a user with Apple Game Center. /// /// The bundle id of the Game Center application. /// The player id of the user in Game Center. /// The URL for the public encryption key. /// A random NSString used to compute the hash and keep it randomized. /// The verification signature data generated. /// The date and time that the signature was created. /// A username used to create the user. May be null. /// If the user should be created when authenticated. /// Extra information that will be bundled in the session token. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a session object. Task AuthenticateGameCenterAsync(string bundleId, string playerId, string publicKeyUrl, string salt, string signature, string timestamp, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Authenticate a user with a Google auth token. /// /// An OAuth access token from the Google SDK. /// A username used to create the user. May be null. /// If the user should be created when authenticated. /// Extra information that will be bundled in the session token. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a session object. Task AuthenticateGoogleAsync(string token, string username = null, bool create = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Authenticate a user with a Steam auth token. /// /// An authentication token from the Steam network. /// A username used to create the user. May be null. /// If the user should be created when authenticated. /// Extra information that will be bundled in the session token. /// If the Steam friends should be imported. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a session object. Task AuthenticateSteamAsync(string token, string username = null, bool create = true, bool import = true, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Ban a set of users from a group. /// /// The session of the user. /// The group to ban the users from. /// The ids of the users to ban. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task BanGroupUsersAsync(ISession session, string groupId, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Block one or more friends by id or username. /// /// The session of the user. /// The ids of the users to block. /// The usernames of the users to block. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task BlockFriendsAsync(ISession session, IEnumerable ids, IEnumerable usernames = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Create a group. /// /// The session of the user. /// The name for the group. /// A description for the group. /// An avatar url for the group. /// A language tag in BCP-47 format for the group. /// If the group should have open membership. /// The maximum number of members allowed. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a new group object. Task CreateGroupAsync(ISession session, string name, string description = "", string avatarUrl = null, string langTag = null, bool open = true, int maxCount = 100, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Delete the current user's account. Note that this will invalidate your session, requiring you to reauthenticate. /// /// The session of the user. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task DeleteAccountAsync(ISession session, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Delete one more or users by id or username from friends. /// /// The session of the user. /// The user ids to remove as friends. /// The usernames to remove as friends. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task DeleteFriendsAsync(ISession session, IEnumerable ids, IEnumerable usernames = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Delete a group by id. /// /// The session of the user. /// The group id to to remove. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task DeleteGroupAsync(ISession session, string groupId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Delete a leaderboard record. /// /// The session of the user. /// The id of the leaderboard with the record to be deleted. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task DeleteLeaderboardRecordAsync(ISession session, string leaderboardId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Delete one or more notifications by id. /// /// The session of the user. /// The notification ids to remove. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task DeleteNotificationsAsync(ISession session, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Delete one or more storage objects. /// /// The session of the user. /// The ids of the objects to delete. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task DeleteStorageObjectsAsync(ISession session, StorageObjectId[] ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Delete the user's tournament record. /// /// The session of the user. /// The id of the tournament to delete from. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task DeleteTournamentRecordAsync(ISession session, string tournamentId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Demote a set of users in a group to the next role down. /// The group to demote users in. /// The users to demote. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// Members who are already at the lowest rank will be skipped. /// A task which represents the asynchronous operation. /// Task DemoteGroupUsersAsync(ISession session, string groupId, IEnumerable userIds, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Submit an event for processing in the server's registered runtime custom events handler. /// /// The session of the user. /// The name of the event. /// The properties of the event. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task EventAsync(ISession session, string name, Dictionary properties, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Fetch the user account owned by the session. /// /// The session of the user. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the account object. Task GetAccountAsync(ISession session, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Get the subscription represented by the provided product id. /// /// The session of the user. /// The product id. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the subscription. Task GetSubscriptionAsync(ISession session, string productId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Fetch one or more users by id, usernames, and Facebook ids. /// /// The session of the user. /// The IDs of the users to retrieve. /// The usernames of the users to retrieve. /// The facebook IDs of the users to retrieve. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a collection of user objects. Task GetUsersAsync(ISession session, IEnumerable ids, IEnumerable usernames = null, IEnumerable facebookIds = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Import Facebook friends and add them to the user's account. /// /// /// The server will import friends when the user authenticates with Facebook. This function can be used to be /// explicit with the import operation. /// /// The session of the user. /// An OAuth access token from the Facebook SDK. /// If the Facebook friend import for the user should be reset. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task ImportFacebookFriendsAsync(ISession session, string token, bool? reset = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Import Steam friends and add them to the user's account. /// /// /// The server will import friends when the user authenticates with Steam. This function can be used to be /// explicit with the import operation. /// /// The session of the user. /// An access token from Steam. /// If the Steam friend import for the user should be reset. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task ImportSteamFriendsAsync(ISession session, string token, bool? reset = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Join a group if it has open membership or request to join it. /// /// The session of the user. /// The ID of the group to join. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task JoinGroupAsync(ISession session, string groupId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Join a tournament by ID. /// /// The session of the user. /// The ID of the tournament to join. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task JoinTournamentAsync(ISession session, string tournamentId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Kick one or more users from the group. /// /// The session of the user. /// The ID of the group. /// The IDs of the users to kick. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task KickGroupUsersAsync(ISession session, string groupId, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Leave a group by ID. /// /// The session of the user. /// The ID of the group to leave. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task LeaveGroupAsync(ISession session, string groupId, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Link an Apple ID to the social profiles on the current user's account. /// /// The session of the user. /// The ID token received from Apple to validate. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task LinkAppleAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Link a custom ID to the user account owned by the session. /// /// The session of the user. /// A custom identifier usually obtained from an external authentication service. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task LinkCustomAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Link a device ID to the user account owned by the session. /// /// The session of the user. /// A device identifier usually obtained from a platform API. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task LinkDeviceAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Link an email with password to the user account owned by the session. /// /// The session of the user. /// The email address of the user. /// The password for the user. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task LinkEmailAsync(ISession session, string email, string password, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Link a Facebook profile to a user account. /// /// The session of the user. /// An OAuth access token from the Facebook SDK. /// If the Facebook friends should be imported. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task LinkFacebookAsync(ISession session, string token, bool? import = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Link a Game Center profile to a user account. /// /// The session of the user. /// The bundle ID of the Game Center application. /// The player ID of the user in Game Center. /// The URL for the public encryption key. /// A random NSString used to compute the hash and keep it randomized. /// The verification signature data generated. /// The date and time that the signature was created. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task LinkGameCenterAsync(ISession session, string bundleId, string playerId, string publicKeyUrl, string salt, string signature, string timestamp, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Link a Google profile to a user account. /// /// The session of the user. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// An OAuth access token from the Google SDK. /// A task which represents the asynchronous operation. Task LinkGoogleAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Link a Steam profile to a user account. /// /// The session of the user. /// An authentication token from the Steam network. /// If the Steam friends should be imported. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task LinkSteamAsync(ISession session, string token, bool import, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List messages from a chat channel. /// /// The session of the user. /// The chat channel object. /// The number of chat messages to list. /// Fetch messages forward from the current cursor (or the start, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default). /// A cursor for the current position in the messages history to list. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the channel message list object. Task ListChannelMessagesAsync(ISession session, IChannel channel, int limit = 1, bool forward = true, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List messages from a chat channel. /// /// The session of the user. /// A channel identifier. /// The number of chat messages to list. /// Fetch messages forward from the current cursor (or the start, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default). /// A cursor for the current position in the messages history to list. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the channel message list object. Task ListChannelMessagesAsync(ISession session, string channelId, int limit = 1, bool forward = true, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List of friends of the current user. /// /// The session of the user. /// Filter by friendship state. /// The number of friends to list. /// A cursor for the current position in the friends list. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the friend objects. Task ListFriendsAsync(ISession session, int? state = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List all users part of the group. /// /// The session of the user. /// The ID of the group. /// Filter by group membership state. /// The number of groups to list. /// A cursor for the current position in the group listing. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the group user objects. Task ListGroupUsersAsync(ISession session, string groupId, int? state = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List groups on the server. /// /// The session of the user. /// The name filter to apply to the group list. /// The number of groups to list. /// A cursor for the current position in the groups to list. /// The language tag filter to apply to the group list. /// The number of group members filter to apply to the group list. /// The open/closed filter to apply to the group list. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task to resolve group objects. Task ListGroupsAsync(ISession session, string name = null, int limit = 1, string cursor = null, string langTag = null, int? members = null, bool? open = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List records from a leaderboard. /// /// The session of the user. /// The ID of the leaderboard to list. /// Record owners to fetch with the list of records. /// Expiry in seconds (since epoch) to begin fetching records from. Optional. 0 means from current time. /// The number of records to list. /// A cursor for the current position in the leaderboard records to list. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the leaderboard record objects. Task ListLeaderboardRecordsAsync(ISession session, string leaderboardId, IEnumerable ownerIds = null, long? expiry = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List leaderboard records that belong to a user. /// /// The session for the user. /// The ID of the leaderboard to list. /// The ID of the user to list around. /// Expiry in seconds (since epoch) to begin fetching records from. Optional. 0 means from current time. /// The limit of the listings. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the leaderboard record objects. Task ListLeaderboardRecordsAroundOwnerAsync(ISession session, string leaderboardId, string ownerId, long? expiry = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Fetch a list of matches active on the server. /// /// The session of the user. /// The minimum number of match participants. /// The maximum number of match participants. /// The number of matches to list. /// If authoritative matches should be included. /// The label to filter the match list on. /// A query for the matches to filter. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the match list object. Task ListMatchesAsync(ISession session, int min, int max, int limit, bool authoritative, string label, string query, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List notifications for the user with an optional cursor. /// /// The session of the user. /// The number of notifications to list. /// A cursor for the current position in notifications to list. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task to resolve notifications objects. Task ListNotificationsAsync(ISession session, int limit = 1, string cacheableCursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); [Obsolete("ListStorageObjects is obsolete, please use ListStorageObjectsAsync instead.", false)] Task ListStorageObjects(ISession session, string collection, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List storage objects in a collection which have public read access. /// /// The session of the user. /// The collection to list over. /// The number of objects to list. Maximum 100. /// A cursor to paginate over the collection. May be null. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the storage object list. Task ListStorageObjectsAsync(ISession session, string collection, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List the user's subscriptions. /// /// The session of the user. /// The number of subscriptions to list. /// An optional cursor for the next page of subscriptions. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the subscription list. Task ListSubscriptionsAsync(ISession session, int limit, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List tournament records around the owner. /// /// The session of the user. /// The ID of the tournament. /// The ID of the owner to pivot around. /// Expiry in seconds (since epoch) to begin fetching records from. /// The number of records to list. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the tournament record list object. Task ListTournamentRecordsAroundOwnerAsync(ISession session, string tournamentId, string ownerId, long? expiry = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List records from a tournament. /// /// The session of the user. /// The ID of the tournament. /// The IDs of the record owners to return in the result. /// Expiry in seconds (since epoch) to begin fetching records from. /// The number of records to list. /// An optional cursor for the next page of tournament records. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the list of tournament records. Task ListTournamentRecordsAsync(ISession session, string tournamentId, IEnumerable ownerIds = null, long? expiry = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List current or upcoming tournaments. /// /// The session of the user. /// The start of the category of tournaments to include. /// The end of the category of tournaments to include. /// The start time of the tournaments. (UNIX timestamp, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default). If null, tournaments will not be filtered by start time. /// The end time of the tournaments. (UNIX timestamp, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default). If null, tournaments will not be filtered by end time. /// The number of tournaments to list. /// An optional cursor for the next page of tournaments. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the list of tournament objects. Task ListTournamentsAsync(ISession session, int categoryStart, int categoryEnd, int? startTime = null, int? endTime = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List of groups the current user is a member of. /// /// The session of the user. /// Filter by group membership state. /// The number of records to list. /// A cursor for the current position in the listing. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the group list object. Task ListUserGroupsAsync(ISession session, int? state = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List groups a user is a member of. /// /// The session of the user. /// The ID of the user whose groups to list. /// Filter by group membership state. /// The number of records to list. /// A cursor for the current position in the listing. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the group list object. Task ListUserGroupsAsync(ISession session, string userId, int? state = null, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List storage objects in a collection which belong to a specific user and have public read access. /// /// The session of the user. /// The collection to list over. /// The user ID of the user to list objects for. /// The number of objects to list. /// A cursor to paginate over the collection. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the storage object list. Task ListUsersStorageObjectsAsync(ISession session, string collection, string userId, int limit = 1, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// List advertised parties and optionally filter them by label. /// /// The session of the user. /// The number of objects to list. /// Optionally filter by party open status. /// Optionally provide a query to filter via custom party labels. /// A cursor to fetch the next page of results, if any. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the party object list. Task ListPartiesAsync(ISession session, int limit, bool? open, string query = null, string cursor = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Promote one or more users in the group. /// /// The session of the user. /// The ID of the group to promote users into. /// The IDs of the users to promote. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task PromoteGroupUsersAsync(ISession session, string groupId, IEnumerable ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Read one or more objects from the storage engine. /// /// The session of the user. /// The objects to read. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the storage batch object. Task ReadStorageObjectsAsync(ISession session, IApiReadStorageObjectId[] ids, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Execute a function with an input payload on the server. /// /// The session of the user. /// The ID of the function to execute on the server. /// The payload to send with the function call. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the RPC response. Task RpcAsync(ISession session, string id, string payload, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Execute a function on the server. /// /// The session of the user. /// The ID of the function to execute on the server. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the RPC response. Task RpcAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Execute a function on the server without a session. /// /// /// This function is usually used with server side code. DO NOT USE client side. /// /// The secure HTTP key used to authenticate. /// The id of the function to execute on the server. /// A payload to send with the function call. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task to resolve an RPC response. Task RpcAsync(string httpKey, string id, string payload, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Execute a function on the server without a session. /// /// /// This function is usually used with server side code. DO NOT USE client side. /// /// The secure HTTP key used to authenticate. /// The id of the function to execute on the server. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task to resolve an RPC response. Task RpcAsync(string httpKey, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Log out a session which invalidates the authorization and refresh token. /// /// The session to logout. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task SessionLogoutAsync(ISession session, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Log out a session which optionally invalidates the authorization and/or refresh tokens. /// /// The authorization token to invalidate, may be null. /// The refresh token to invalidate, may be null. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task SessionLogoutAsync(string authToken, string refreshToken, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Refresh the session unless the current refresh token has expired. If vars are specified they will replace /// what is currently stored inside the session token. /// /// The session of the user. /// Extra information which should be bundled inside the session token. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a new session object. Task SessionRefreshAsync(ISession session, Dictionary vars = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Remove the Apple ID from the social profiles on the current user's account. /// /// The session of the user. /// The ID token received from Apple. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a new session object. Task UnlinkAppleAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Unlink a custom ID from the user account owned by the session. /// /// The session of the user. /// A custom identifier usually obtained from an external authentication service. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task UnlinkCustomAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Unlink a device ID from the user account owned by the session. /// /// The session of the user. /// A device identifier usually obtained from a platform API. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task UnlinkDeviceAsync(ISession session, string id, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Unlink an email with password from the user account owned by the session. /// /// The session of the user. /// The email address of the user. /// The password for the user. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task UnlinkEmailAsync(ISession session, string email, string password, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Unlink a Facebook profile from the user account owned by the session. /// /// The session of the user. /// An OAuth access token from the Facebook SDK. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task UnlinkFacebookAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Unlink a Game Center profile from the user account owned by the session. /// /// The session of the user. /// The bundle ID of the Game Center application. /// The player ID of the user in Game Center. /// The URL for the public encryption key. /// A random NSString used to compute the hash and keep it randomized. /// The verification signature data generated. /// The date and time that the signature was created. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task UnlinkGameCenterAsync(ISession session, string bundleId, string playerId, string publicKeyUrl, string salt, string signature, string timestamp, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Unlink a Google profile from the user account owned by the session. /// /// The session of the user. /// An OAuth access token from the Google SDK. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task UnlinkGoogleAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Unlink a Steam profile from the user account owned by the session. /// /// The session of the user. /// An authentication token from the Steam network. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task UnlinkSteamAsync(ISession session, string token, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Update the current user's account on the server. /// /// The session for the user. /// The new username for the user. /// A new display name for the user. /// A new avatar url for the user. /// A new language tag in BCP-47 format for the user. /// A new location for the user. /// New timezone information for the user. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task UpdateAccountAsync(ISession session, string username, string displayName = null, string avatarUrl = null, string langTag = null, string location = null, string timezone = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Update a group. /// /// /// The user must have the correct access permissions for the group. /// /// The session of the user. /// The ID of the group to update. /// A new name for the group. /// If the group should have open membership. /// A new description for the group. /// A new avatar url for the group. /// A new language tag in BCP-47 format for the group. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which represents the asynchronous operation. Task UpdateGroupAsync(ISession session, string groupId, string name, bool open, string description = null, string avatarUrl = null, string langTag = null, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Validate a purchase receipt against the Apple App Store. /// /// The session of the user. /// The purchase receipt to be validated. /// Whether or not to track the receipt in the Nakama database. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the validated list of purchase receipts. Task ValidatePurchaseAppleAsync(ISession session, string receipt, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Validate a purchase receipt against Facebook Instant Games. /// /// The session of the user. /// Base64 encoded Facebook Instant receipt data payload. /// Whether or not to track the receipt in the Nakama database. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the validated list of purchase receipts. Task ValidatePurchaseFacebookInstantAsync(ISession session, string signedRequest, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Validate a purchase receipt against the Google Play Store. /// /// The session of the user. /// The purchase receipt to be validated. /// Whether or not to track the receipt in the Nakama database. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the validated list of purchase receipts. Task ValidatePurchaseGoogleAsync(ISession session, string receipt, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Validate a purchase receipt against the Huawei AppGallery. /// /// The session of the user. /// The purchase receipt to be validated. /// The signature of the purchase receipt. /// Whether or not to track the receipt in the Nakama database. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the validated list of purchase receipts. Task ValidatePurchaseHuaweiAsync(ISession session, string receipt, string signature, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Validate an Apple subscription receipt. /// /// The session of the user. /// The receipt to validate. /// Whether or not to persist the receipt to Nakama's database. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the subscription validation response. Task ValidateSubscriptionAppleAsync(ISession session, string receipt, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Validate a Google subscription receipt. /// /// The session of the user. /// The receipt to validate. /// Whether or not to persist the receipt to Nakama's database. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the subscription validation response. Task ValidateSubscriptionGoogleAsync(ISession session, string receipt, bool persist = true, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Write a record to a leaderboard. /// /// The session for the user. /// The ID of the leaderboard to write. /// The score for the leaderboard record. /// The sub score for the leaderboard record. /// The metadata for the leaderboard record. /// The operator for the record that can be used to override the one set in the leaderboard. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the leaderboard record object written. Task WriteLeaderboardRecordAsync(ISession session, string leaderboardId, long score, long subScore = 0L, string metadata = null, ApiOperator apiOperator = ApiOperator.NO_OVERRIDE, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Write objects to the storage engine. /// /// The session of the user. /// The objects to write. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the storage write acknowledgements. Task WriteStorageObjectsAsync(ISession session, IApiWriteStorageObject[] objects, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); /// /// Write a record to a tournament. /// /// The session of the user. /// The ID of the tournament to write. /// The score of the tournament record. /// The sub score for the tournament record. /// The metadata for the tournament record. /// The operator for the record that can be used to override the one set in the tournament. /// The retry configuration. See /// The that can be used to cancel the request while mid-flight. /// A task which resolves to the tournament record object written. Task WriteTournamentRecordAsync(ISession session, string tournamentId, long score, long subScore = 0L, string metadata = null, ApiOperator apiOperator = ApiOperator.NO_OVERRIDE, RetryConfiguration retryConfiguration = null, CancellationToken canceller = default); } } ================================================ FILE: Nakama/IHttpAdapter.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Nakama { /// /// An adapter which implements the HTTP protocol. /// public interface IHttpAdapter { // A delegate used by the adapter to determine whether or not an error from the server // should be retried or not (i.e., is 'transient'). TransientExceptionDelegate TransientExceptionDelegate { get; } /// /// The logger to use with the adapter. /// ILogger Logger { get; set; } /// /// Send a HTTP request. /// /// HTTP method to use for this request. /// The fully qualified URI to use. /// Request headers to set. /// Request content body to set. /// Request timeout. /// A user-generated token that can be used to cancel the request. /// A task which resolves to the contents of the response. Task SendAsync(string method, Uri uri, IDictionary headers, byte[] body, int timeoutSec = 3, CancellationToken? userCancelToken = null); } } ================================================ FILE: Nakama/IHttpAdapterUtil.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; namespace Nakama { /// /// Utility methods for the interface. /// NOTE: DO NOT USE EXTENSION METHODS as Unity cannot cross-compile /// them properly to WebGL. /// public static class IHttpAdapterUtil { /// /// Performs an in-place copy of data from Nakama's error response into /// the data dictionary of an . /// /// The adapter receiving the error response. /// The decoded error field from the server response. /// The exception whose data dictionary is being written to. public static void CopyResponseError(IHttpAdapter adapter, object err, ApiResponseException e) { var errString = err as string; var errDict = err as Dictionary; if (errString != null) { e.Data["error"] = err; } else if (errDict != null) { foreach (KeyValuePair keyVal in errDict) { e.Data[keyVal.Key] = keyVal.Value; } } } } } ================================================ FILE: Nakama/ILogger.cs ================================================ // Copyright 2018 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Nakama { /// /// A simple logger to write log messages to an output sink. /// public interface ILogger { /// /// Logs a formatted string with the DEBUG level. /// /// A string with zero or more format items. /// An object array with zero or more objects to format. void DebugFormat(string format, params object[] args); /// /// Logs a formatted string with the ERROR level. /// /// A string with zero or more format items. /// An object array with zero or more objects to format. void ErrorFormat(string format, params object[] args); /// /// Logs a formatted string with the INFO level. /// /// A string with zero or more format items. /// An object array with zero or more objects to format. void InfoFormat(string format, params object[] args); /// /// Logs a formatted string with the WARN level. /// /// A string with zero or more format items. /// An object array with zero or more objects to format. void WarnFormat(string format, params object[] args); } } ================================================ FILE: Nakama/IMatch.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// A multiplayer match. /// public interface IMatch { /// /// If this match has an authoritative handler on the server. /// bool Authoritative { get; } /// /// The unique match identifier. /// string Id { get; } /// /// A label for the match which can be filtered on. /// string Label { get; } /// /// The presences already in the match. /// IEnumerable Presences { get; } /// /// The number of users currently in the match. /// int Size { get; } /// /// The current user in this match. i.e. Yourself. /// IUserPresence Self { get; } /// /// Apply the joins and leaves from a presence event to the presences tracked by the match. /// void UpdatePresences(IMatchPresenceEvent presenceEvent); } /// internal class Match : IMatch { [DataMember(Name = "authoritative"), Preserve] public bool Authoritative { get; set; } [DataMember(Name = "match_id"), Preserve] public string Id { get; set; } [DataMember(Name = "label"), Preserve] public string Label { get; set; } public IEnumerable Presences => _presences ?? UserPresence.NoPresences; [DataMember(Name = "presences"), Preserve] public List _presences { get; set; } [DataMember(Name = "size"), Preserve] public int Size { get; set; } public IUserPresence Self => _self; [DataMember(Name = "self"), Preserve] public UserPresence _self { get; set; } public override string ToString() { var presences = string.Join(", ", Presences); return $"Match(Authoritative={Authoritative}, Id='{Id}', Label='{Label}', Presences=[{presences}], Size={Size}, Self={Self})"; } public void UpdatePresences(IMatchPresenceEvent presenceEvent) { if (presenceEvent.MatchId != Id) { throw new InvalidOperationException("Tried updating presences belonging to the wrong match."); } _presences = PresenceUtil.CopyJoinsAndLeaves(_presences, presenceEvent.Joins, presenceEvent.Leaves); } } } ================================================ FILE: Nakama/IMatchPresenceEvent.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// A batch of join and leave presences for a match. /// public interface IMatchPresenceEvent { /// /// Presences of users who joined the match. /// IEnumerable Joins { get; } /// /// Presences of users who left the match. /// IEnumerable Leaves { get; } /// /// The unique match identifier. /// string MatchId { get; } } /// internal class MatchPresenceEvent : IMatchPresenceEvent { public IEnumerable Joins => _joins ?? UserPresence.NoPresences; [DataMember(Name = "joins"), Preserve] public List _joins { get; set; } public IEnumerable Leaves => _leaves ?? UserPresence.NoPresences; [DataMember(Name = "leaves"), Preserve] public List _leaves { get; set; } [DataMember(Name = "match_id"), Preserve] public string MatchId { get; set; } public override string ToString() { var joins = string.Join(", ", Joins); var leaves = string.Join(", ", Leaves); return $"MatchPresenceEvent(Joins=[{joins}], Leaves=[{leaves}], MatchId='{MatchId}')"; } } } ================================================ FILE: Nakama/IMatchState.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Runtime.Serialization; namespace Nakama { /// /// Some game state update in a match. /// public interface IMatchState { /// /// The unique match identifier. /// string MatchId { get; } /// /// The operation code for the state change. /// /// /// This value can be used to mark the type of the contents of the state. /// long OpCode { get; } /// /// The byte contents of the state change. /// byte[] State { get; } /// /// Information on the user who sent the state change. /// IUserPresence UserPresence { get; } } /// internal class MatchState : IMatchState { private static readonly byte[] NoBytes = new byte[0]; [DataMember(Name = "match_id"), Preserve] public string MatchId { get; set; } public long OpCode => Convert.ToInt64(OpCodeField); [DataMember(Name = "op_code"), Preserve] public string OpCodeField { get; set; } public byte[] State => StateField == null ? NoBytes : Convert.FromBase64String(StateField); [DataMember(Name = "data"), Preserve] public string StateField { get; set; } public IUserPresence UserPresence => UserPresenceField; [DataMember(Name = "presence"), Preserve] public UserPresence UserPresenceField { get; set; } public override string ToString() { return $"MatchState(MatchId='{MatchId}', OpCode={OpCode}, State='{State}', UserPresence={UserPresence})"; } } } ================================================ FILE: Nakama/IMatchmakerMatched.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// The result of a successful matchmaker operation sent to the server. /// public interface IMatchmakerMatched { /// /// The id used to join the match. /// /// /// A match ID used to join the match. /// string MatchId { get; } /// /// The ticket sent by the server when the user requested to matchmake for other players. /// string Ticket { get; } /// /// The token used to join a match. /// string Token { get; } /// /// The other users matched with this user and the parameters they sent. /// IEnumerable Users { get; } /// /// The current user who matched with opponents. /// IMatchmakerUser Self { get; } } /// /// The user with the parameters they sent to the server when asking for opponents. /// public interface IMatchmakerUser { /// /// The numeric properties which this user asked to matchmake with. /// IDictionary NumericProperties { get; } /// /// The presence of the user. /// IUserPresence Presence { get; } /// /// The string properties which this user asked to matchmake with. /// IDictionary StringProperties { get; } } /// internal class MatchmakerMatched : IMatchmakerMatched { [DataMember(Name = "match_id"), Preserve] public string MatchId { get; set; } [DataMember(Name = "ticket"), Preserve] public string Ticket { get; set; } [DataMember(Name = "token"), Preserve] public string Token { get; set; } public IEnumerable Users => _users ?? new List(0); [DataMember(Name = "users"), Preserve] public List _users { get; set; } public IMatchmakerUser Self => _self; [DataMember(Name = "self"), Preserve] public MatchmakerUser _self { get; set; } public override string ToString() { var users = string.Join(", ", Users); return $"MatchmakerMatched(MatchId='{MatchId}', Ticket='{Ticket}', Token='{Token}', Users=[{users}], Self={Self})"; } } /// internal class MatchmakerUser : IMatchmakerUser { public IDictionary NumericProperties => _numericProperties ?? new Dictionary(); [DataMember(Name = "numeric_properties"), Preserve] public Dictionary _numericProperties { get; set; } public IUserPresence Presence => _presence; [DataMember(Name = "presence"), Preserve] public UserPresence _presence { get; set; } public IDictionary StringProperties => _stringProperties ?? new Dictionary(); [DataMember(Name = "string_properties"), Preserve] public Dictionary _stringProperties { get; set; } public override string ToString() { return $"MatchmakerUser(NumericProperties={NumericProperties}, Presence={Presence}, StringProperties={StringProperties})"; } } } ================================================ FILE: Nakama/IMatchmakerTicket.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// The matchmaker ticket received from the server. /// public interface IMatchmakerTicket { /// /// The ticket generated by the matchmaker. /// string Ticket { get; } } /// internal class MatchmakerTicket : IMatchmakerTicket { [DataMember(Name="ticket"), Preserve] public string Ticket { get; set; } public override string ToString() { return $"MatchmakerTicket(Ticket='{Ticket}')"; } } } ================================================ FILE: Nakama/IParty.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; namespace Nakama { /// /// Incoming information about a party. /// public interface IParty { /// /// The unique party identifier. /// string Id { get; } /// /// True, if the party is open to join. /// bool Open { get; } /// /// True, if the party is hidden from listing. /// bool Hidden { get; } /// /// The maximum number of party members. /// int MaxSize { get; } /// /// Label to filter results in Party listing. /// string Label { get; } /// /// The current user in this party. i.e. Yourself. /// IUserPresence Self { get; } /// /// The current party leader. /// IUserPresence Leader { get; } /// /// All members currently in the party. /// IEnumerable Presences { get; } /// /// Apply the joins and leaves from a presence event to the presences tracked by the party. /// void UpdatePresences(IPartyPresenceEvent presenceEvent); } } ================================================ FILE: Nakama/IPartyClose.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Nakama { /// /// End a party, kicking all party members and closing it. /// public interface IPartyClose { /// /// The ID of the party to close. /// string PartyId { get; } } } ================================================ FILE: Nakama/IPartyData.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Nakama { /// /// Incoming party data delivered from the server. /// public interface IPartyData { /// /// The ID of the party. /// string PartyId { get; } /// /// A reference to the user presence that sent this data, if any. /// IUserPresence Presence { get; } /// /// The operation code the message was sent with. /// long OpCode { get; } /// /// Data payload, if any. /// byte[] Data { get; } } } ================================================ FILE: Nakama/IPartyJoinRequest.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; namespace Nakama { /// /// Incoming notification for one or more new presences attempting to join the party. /// public interface IPartyJoinRequest { /// /// The ID of the party to get a list of join requests for. /// string PartyId { get; } /// /// Presences attempting to join, or who have joined. /// IEnumerable Presences { get; } } } ================================================ FILE: Nakama/IPartyLeader.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Nakama { /// /// Announcement of a new party leader. /// public interface IPartyLeader { /// /// The ID of the party to announce the new leader for. /// string PartyId { get; } /// /// The presence of the new party leader. /// IUserPresence Presence { get; } } } ================================================ FILE: Nakama/IPartyMatchmakerTicket.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Nakama { /// /// A response from starting a new party matchmaking process. /// public interface IPartyMatchmakerTicket { /// /// The ID of the party. /// string PartyId { get; } /// /// The ticket that can be used to cancel matchmaking. /// string Ticket { get; } } } ================================================ FILE: Nakama/IPartyPresenceEvent.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; namespace Nakama { /// /// Presence update for a particular party. /// public interface IPartyPresenceEvent { /// /// The ID of the party. /// string PartyId { get; } /// /// The user presences that have just joined the party. /// IEnumerable Joins { get; } /// /// The user presences that have just left the party. /// IEnumerable Leaves { get; } } } ================================================ FILE: Nakama/IPartyUpdate.cs ================================================ // Copyright 2025 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; namespace Nakama { /// /// Incoming information about a party. /// public interface IPartyUpdate { /// /// The unique party identifier. /// string PartyId { get; } /// /// True, if the party is open to join. /// bool Open { get; } /// /// True, if the party is show in listing. /// bool Hidden { get; } /// /// Label to filter results in Party listing. /// string Label { get; } } } ================================================ FILE: Nakama/ISession.cs ================================================ // Copyright 2019 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; namespace Nakama { /// /// A session authenticated for a user with Nakama server. /// public interface ISession { /// /// The authorization token used to construct this session. /// string AuthToken { get; } /// /// If the user account for this session was just created. /// bool Created { get; } /// /// The UNIX timestamp when this session was created. /// long CreateTime { get; } /// /// The UNIX timestamp when this session will expire. /// long ExpireTime { get; } /// /// If the session has expired. /// bool IsExpired { get; } /// /// If the refresh token has expired. /// bool IsRefreshExpired { get; } /// /// The UNIX timestamp when the refresh token will expire. /// long RefreshExpireTime { get; } /// /// Refresh token that can be used for session token renewal. /// string RefreshToken { get; } /// /// Any custom properties associated with this session. /// IDictionary Vars { get; } /// /// The username of the user who owns this session. /// string Username { get; } /// /// The ID of the user who owns this session. /// string UserId { get; } /// /// Check the session has expired against the offset time. /// /// The datetime to compare against this session. /// If the session has expired. bool HasExpired(DateTime offset); /// /// Check if the refresh token has expired against the offset time. /// /// The datetime to compare against this refresh token. /// If refresh token has expired. bool HasRefreshExpired(DateTime offset); } } ================================================ FILE: Nakama/ISocket.cs ================================================ // Copyright 2019 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Nakama { /// /// A socket to interact with Nakama server. /// public interface ISocket { /// /// Received when a socket is closed. /// event Action Closed; /// /// Received when a socket is connected. /// event Action Connected; /// /// Received a chat channel message. /// event Action ReceivedChannelMessage; /// /// Received a presence change for joins and leaves with users in a chat channel. /// event Action ReceivedChannelPresence; /// /// Received when an error occurs on the socket. /// event Action ReceivedError; /// /// Received a matchmaker matched message. /// event Action ReceivedMatchmakerMatched; /// /// Received a message from a multiplayer match. /// event Action ReceivedMatchState; /// /// Received a presence change for joins and leaves of users in a multiplayer match. /// event Action ReceivedMatchPresence; /// /// Received a notification for the current user. /// event Action ReceivedNotification; /// /// Received a party event. This will occur when the current user's invitation request is accepted /// by the party leader of a closed party. /// event Action ReceivedParty; /// /// Received a party close event. /// event Action ReceivedPartyClose; /// /// Received custom party data. /// event Action ReceivedPartyData; /// /// Received a request to join the party. /// event Action ReceivedPartyJoinRequest; /// /// Received a change in the party leader. /// event Action ReceivedPartyLeader; /// /// Received a new matchmaker ticket for the party. /// event Action ReceivedPartyMatchmakerTicket; /// /// Received a new presence event in the party. /// event Action ReceivedPartyPresence; /// /// Received a party label and/or open/closed change. /// event Action ReceivedPartyUpdate; /// /// Received a presence change for when a user updated their online status. /// event Action ReceivedStatusPresence; /// /// Received a presence change for joins and leaves on a realtime stream. /// event Action ReceivedStreamPresence; /// /// Received a message from a realtime stream. /// event Action ReceivedStreamState; /// /// If the socket is connected. /// bool IsConnected { get; } /// /// If the socket is connecting. /// bool IsConnecting { get; } /// /// Accept a party member's request to join the party. /// /// The party ID to accept the join request for. /// The presence to accept as a party member. /// A task to represent the asynchronous operation. Task AcceptPartyMemberAsync(string partyId, IUserPresence presence); /// /// Join the matchmaker pool and search for opponents on the server. /// /// The matchmaker query to search for opponents. /// The minimum number of players to compete against in a match. /// The maximum number of players to compete against in a match. /// A set of key/value properties to provide to searches. /// A set of key/value numeric properties to provide to searches. /// An optional integer to force the matchmaker to match in multiples of. /// A task which resolves to a matchmaker ticket object. Task AddMatchmakerAsync(string query = "*", int minCount = 2, int maxCount = 8, Dictionary stringProperties = null, Dictionary numericProperties = null, int? countMultiple = null); /// /// Begin matchmaking as a party. /// /// Party ID. /// Filter query used to identify suitable users. /// Minimum total user count to match together. /// Maximum total user count to match together. /// String properties. /// Numeric properties. /// An optional integer to force the matchmaker to match in multiples of. /// A task which resolves to a party matchmaker ticket object. Task AddMatchmakerPartyAsync(string partyId, string query, int minCount, int maxCount, Dictionary stringProperties = null, Dictionary numericProperties = null, int? countMultiple = null); /// /// End a party, kicking all party members and closing it. /// /// The ID of the party. /// A task to represent the asynchronous operation. Task ClosePartyAsync(string partyId); /// /// Close the socket connection to the server. /// /// A task to represent the asynchronous operation. Task CloseAsync(); /// /// Connect to the server. /// /// The session of the user. /// If the user who appear online to other users. /// The time allowed for the socket connection to be established. /// The language tag of the user on the connected socket. /// A task to represent the asynchronous operation. Task ConnectAsync(ISession session, bool appearOnline = false, int connectTimeout = Socket.DefaultConnectTimeout, string langTag = "en"); /// /// Create a multiplayer match on the server. /// /// A task to represent the asynchronous operation. Task CreateMatchAsync(string matchName = null); /// /// Create a party. /// /// Whether the party will require join requests to be approved by the party leader. /// Whether the party should be hidden from client listing. /// Maximum number of party members. /// An optional label to set for party listing. /// A task to represent the asynchronous operation. Task CreatePartyAsync(bool open, bool hidden, int maxSize, string label = null); /// /// Subscribe to one or more users for their status updates. /// /// The users. /// A task which resolves to the current statuses for the users. Task FollowUsersAsync(IEnumerable users); /// /// Subscribe to one or more users for their status updates. /// /// The IDs of users. /// The usernames of the users. /// A task which resolves to the current statuses for the users. Task FollowUsersAsync(IEnumerable userIDs, IEnumerable usernames = null); /// /// Join a chat channel on the server. /// /// The target channel to join. /// The type of channel to join. /// If chat messages should be stored. /// If the current user should be hidden on the channel. /// A task which resolves to a chat channel object. Task JoinChatAsync(string target, ChannelType type, bool persistence = false, bool hidden = false); /// /// Join a party. /// /// Party ID /// A task to represent the asynchronous operation. Task JoinPartyAsync(string partyId); /// /// Join a multiplayer match with the matchmaker matched object. /// /// A matchmaker matched object. /// A task which resolves to a multiplayer match. Task JoinMatchAsync(IMatchmakerMatched matched); /// /// Join a multiplayer match by ID. /// /// The ID of the match to attempt to join. /// An optional set of key-value metadata pairs to be passed to the match handler. /// A task which resolves to a multiplayer match. Task JoinMatchAsync(string matchId, IDictionary metadata = null); /// /// Leave a chat channel on the server. /// /// The chat channel to leave. /// A task which represents the asynchronous operation. Task LeaveChatAsync(IChannel channel); /// /// Leave a chat channel on the server. /// /// The ID of the chat channel to leave. /// A task which represents the asynchronous operation. Task LeaveChatAsync(string channelId); /// /// Leave a multiplayer match on the server. /// /// The multiplayer match to leave. /// A task which represents the asynchronous operation. Task LeaveMatchAsync(IMatch match); /// /// Leave a multiplayer match on the server. /// /// The multiplayer match to leave. /// A task which represents the asynchronous operation. Task LeaveMatchAsync(string matchId); /// /// Leave the party. /// /// Party ID. /// A task to represent the asynchronous operation. Task LeavePartyAsync(string partyId); /// /// Request a list of pending join requests for a party. /// /// Party ID. /// A task which resolves to a list of all party join requests. Task ListPartyJoinRequestsAsync(string partyId); /// /// Promote a new party leader. /// /// Party ID. /// The presence of an existing party member to promote as the new leader. /// A task which resolves to an announcement of a new party leader. Task PromotePartyMemberAsync(string partyId, IUserPresence partyMember); /// /// Remove a chat message from a chat channel on the server. /// /// The chat channel with the message to remove. /// The ID of the chat message to remove. /// A task which resolves to an acknowledgement of the removed message. Task RemoveChatMessageAsync(IChannel channel, string messageId); /// /// Remove a chat message from a chat channel on the server. /// /// The ID of the chat channel with the message to remove. /// The ID of the chat message to remove. /// A task which resolves to an acknowledgement of the removed message. Task RemoveChatMessageAsync(string channelId, string messageId); /// /// Leave the matchmaker pool with the ticket. /// /// The ticket returned by the matchmaker on join. /// A task which represents the asynchronous operation. Task RemoveMatchmakerAsync(IMatchmakerTicket ticket); /// /// Leave the matchmaker pool with the ticket contents. /// /// The contents of the ticket returned by the matchmaker on join. /// A task which represents the asynchronous operation. Task RemoveMatchmakerAsync(string ticket); /// /// Cancel a party matchmaking process using a ticket. /// /// Party ID. /// The ticket to cancel. /// A task which represents the asynchronous operation. Task RemoveMatchmakerPartyAsync(string partyId, string ticket); /// /// Kick a party member, or decline a request to join. /// /// Party ID to remove/reject from. /// The presence to remove or reject. /// A task which represents the asynchronous operation. Task RemovePartyMemberAsync(string partyId, IUserPresence presence); /// /// Execute an RPC function to the server. /// /// The ID of the function to execute. /// An (optional) payload to send to the server. /// A task which resolves to the RPC function response object. Task RpcAsync(string funcId, string payload = null); /// /// Execute an RPC function to the server. /// /// The ID of the function to execute. /// An (optional) payload sent to the server from the byte buffer. /// A task which resolves to the RPC function response object. Task RpcAsync(string funcId, ArraySegment payload); /// /// Send input to a multiplayer match on the server. /// /// /// /// When no presences are supplied the new match state will be sent to all presences. /// /// The ID of the match. /// An operation code for the input. /// The input data to send. /// The presences in the match who should receive the input. /// A task which represents the asynchronous operation. Task SendMatchStateAsync(string matchId, long opCode, string state, IEnumerable presences = null); /// /// Send input to a multiplayer match on the server. /// /// The ID of the match. /// An operation code for the input. /// The input data to send from the byte buffer. /// The presences in the match who should receive the input. /// A task which represents the asynchronous operation. Task SendMatchStateAsync(string matchId, long opCode, ArraySegment state, IEnumerable presences = null); /// /// Send input to a multiplayer match on the server. /// /// /// /// When no presences are supplied the new match state will be sent to all presences. /// /// The ID of the match. /// An operation code for the input. /// The input data to send. /// The presences in the match who should receive the input. /// A task which represents the asynchronous operation. Task SendMatchStateAsync(string matchId, long opCode, byte[] state, IEnumerable presences = null); /// /// Send data to a party. /// /// Party ID to send to. /// Op code value. /// The input data to send from the byte buffer, if any. /// A task which represents the asynchronous operation. Task SendPartyDataAsync(string partyId, long opCode, ArraySegment data); /// /// Send data to a party. /// /// Party ID to send to. /// Op code value. /// Data payload, if any. /// A task which represents the asynchronous operation. Task SendPartyDataAsync(string partyId, long opCode, string data); /// /// Send data to a party. /// /// Party ID to send to. /// Op code value. /// Data payload, if any. /// A task which represents the asynchronous operation. Task SendPartyDataAsync(string partyId, long opCode, byte[] data); /// /// Unfollow one or more users from their status updates. /// /// The users to unfollow. /// A task which represents the asynchronous operation. Task UnfollowUsersAsync(IEnumerable users); /// /// Unfollow one or more users from their status updates. /// /// The IDs of the users to unfollow. /// A task which represents the asynchronous operation. Task UnfollowUsersAsync(IEnumerable userIDs); /// /// Update a chat message on a chat channel in the server. /// /// The chat channel with the message to update. /// The ID of the message to update. /// The new contents of the chat message. /// A task which resolves to an acknowledgement of the updated message. Task UpdateChatMessageAsync(IChannel channel, string messageId, string content); /// /// Update a chat message on a chat channel in the server. /// /// The ID of the chat channel with the message to update. /// The ID of the message to update. /// The new contents of the chat message. /// A task which resolves to an acknowledgement of the updated message. Task UpdateChatMessageAsync(string channelId, string messageId, string content); /// /// Update party label and optionally whether it is open or closed. /// /// The Party ID. /// Whether the party is open or closed. /// Whether the party should be hidden from client listing. /// The new custom label to set to the party. /// Task UpdatePartyAsync(string partyId, bool open, bool hidden, string label = ""); /// /// Update the status for the current user online. /// /// The new status for the user. /// A task which represents the asynchronous operation. Task UpdateStatusAsync(string status); /// /// Send a chat message to a chat channel on the server. /// /// The chat channel to send onto. /// The contents of the message to send. /// A task which resolves to the acknowledgement of the chat message write. Task WriteChatMessageAsync(IChannel channel, string content); /// /// Send a chat message to a chat channel on the server. /// /// The ID of the chat channel to send onto. /// The contents of the message to send. /// A task which resolves to the acknowledgement of the chat message write. Task WriteChatMessageAsync(string channelId, string content); } } ================================================ FILE: Nakama/ISocketAdapter.cs ================================================ // Copyright 2019 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Threading; using System.Threading.Tasks; namespace Nakama { /// /// An adapter which implements a socket with a protocol supported by Nakama. /// public interface ISocketAdapter { /// /// An event dispatched when the socket is connected. /// event Action Connected; /// /// An event dispatched when the socket is disconnected. /// event Action Closed; /// /// An event dispatched when the socket has an error when connected. /// event Action ReceivedError; /// /// An event dispatched when the socket receives a message. /// event Action> Received; /// /// If the socket is connected. /// bool IsConnected { get; } /// /// If the socket is connecting. /// bool IsConnecting { get; } /// /// Close the socket with an asynchronous operation. /// Task CloseAsync(); /// /// Connect to the server with an asynchronous operation. /// /// The URI of the server. /// The timeout for the connect attempt on the socket. Task ConnectAsync(Uri uri, int timeout); /// /// Send data to the server with an asynchronous operation. /// /// The buffer with the message to send. /// If the message should be sent reliably (will be ignored by some protocols). /// A cancellation token used to propagate when the operation should be canceled. Task SendAsync(ArraySegment buffer, bool reliable = true, CancellationToken canceller = default); } } ================================================ FILE: Nakama/IStatus.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// Receive status updates for users. /// public interface IStatus { /// /// The status events for the users followed. /// IEnumerable Presences { get; } } /// internal class Status : IStatus { public IEnumerable Presences => PresencesField ?? UserPresence.NoPresences; [DataMember(Name="presences"), Preserve] public List PresencesField { get; set; } public override string ToString() { var presences = string.Join(", ", Presences); return $"Status(Presences=[{presences}])"; } } } ================================================ FILE: Nakama/IStatusPresenceEvent.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// A status update event about other users who've come online or gone offline. /// public interface IStatusPresenceEvent { /// /// Presences of users who left the server. /// /// /// This leave information is in response to a subscription made to be notified when a user goes offline. /// IEnumerable Leaves { get; } /// /// Presences of users who joined the server. /// /// /// This join information is in response to a subscription made to be notified when a user comes online. /// IEnumerable Joins { get; } } /// internal class StatusPresenceEvent : IStatusPresenceEvent { public IEnumerable Leaves => _leaves ?? UserPresence.NoPresences; [DataMember(Name = "leaves"), Preserve] public List _leaves { get; set; } public IEnumerable Joins => _joins ?? UserPresence.NoPresences; [DataMember(Name = "joins"), Preserve] public List _joins { get; set; } public override string ToString() { var joins = string.Join(", ", Joins); var leaves = string.Join(", ", Leaves); return $"StatusPresenceEvent(Leaves=[{leaves}], Joins=[{joins}])"; } } } ================================================ FILE: Nakama/IStreamPresenceEvent.cs ================================================ // Copyright 2018 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; using System.Runtime.Serialization; using static System.String; namespace Nakama { /// /// A batch of joins and leaves on the low level stream. /// /// /// Streams are built on to provide abstractions for matches, chat channels, etc. In most cases you'll never need to /// interact with the low level stream itself. /// public interface IStreamPresenceEvent { /// /// Presences of users who joined the stream. /// IEnumerable Leaves { get; } /// /// Presences of users who left the stream. /// IEnumerable Joins { get; } /// /// The identifier for the stream. /// IStream Stream { get; } } /// /// A state change received from a stream. /// public interface IStreamState { /// /// The user who sent the state change. May be null. /// IUserPresence Sender { get; } /// /// The contents of the state change. /// string State { get; } /// /// The identifier for the stream. /// IStream Stream { get; } } /// /// A realtime socket stream on the server. /// public interface IStream { /// /// The descriptor of the stream. Used with direct chat messages and contains a second user id. /// string Descriptor { get; } /// /// Identifies streams which have a context across users like a chat channel room. /// string Label { get; } /// /// The mode of the stream. /// int Mode { get; } /// /// The subject of the stream. This is usually a user id. /// string Subject { get; } } /// [Preserve] internal class StreamPresenceEvent : IStreamPresenceEvent { public IEnumerable Leaves => LeavesField ?? UserPresence.NoPresences; [DataMember(Name = "leaves"), Preserve] public List LeavesField { get; set; } public IEnumerable Joins => JoinsField ?? UserPresence.NoPresences; [DataMember(Name = "joins"), Preserve] public List JoinsField { get; set; } public IStream Stream => StreamField; [DataMember(Name = "stream"), Preserve] public Stream StreamField { get; set; } public override string ToString() => $"StreamPresenceEvent(Leaves=[{Join(", ", Leaves)}], Joins=[{Join(", ", Joins)}], Stream={Stream})"; } /// [Preserve] internal class StreamState : IStreamState { public IUserPresence Sender => SenderField; [DataMember(Name = "sender"), Preserve] public UserPresence SenderField { get; set; } public string State => StateField; [DataMember(Name = "data"), Preserve] public string StateField { get; set; } public IStream Stream => StreamField; [DataMember(Name = "stream"), Preserve] public Stream StreamField { get; set; } public override string ToString() => $"StreamState(Sender={Sender}, State='{StateField}', Stream={Stream})"; } /// [Preserve] internal class Stream : IStream { [DataMember(Name = "descriptor"), Preserve] public string Descriptor { get; set; } [DataMember(Name = "label"), Preserve] public string Label { get; set; } [DataMember(Name = "mode"), Preserve] public int Mode { get; set; } [DataMember(Name = "subject"), Preserve] public string Subject { get; set; } public override string ToString() => $"Stream(Descriptor='{Descriptor}', Label='{Label}', Mode={Mode}, Subject='{Subject}')"; } } ================================================ FILE: Nakama/IUserPresence.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// An object which represents a connected user in the server. /// /// /// The server allows the same user to be connected with multiple sessions. To uniquely identify them a tuple of /// { node_id, user_id, session_id } is used which is exposed as this object. /// public interface IUserPresence { /// /// If this presence generates stored events like persistent chat messages or notifications. /// bool Persistence { get; } /// /// The session id of the user. /// string SessionId { get; } /// /// The status of the user with the presence on the server. /// string Status { get; } /// /// The username for the user. /// string Username { get; } /// /// The id of the user. /// string UserId { get; } } /// internal class UserPresence : IUserPresence { internal static readonly IReadOnlyList NoPresences = new List(0); [DataMember(Name = "persistence"), Preserve] public bool Persistence { get; set; } [DataMember(Name = "session_id"), Preserve] public string SessionId { get; set; } [DataMember(Name = "status"), Preserve] public string Status { get; set; } [DataMember(Name = "username"), Preserve] public string Username { get; set; } [DataMember(Name = "user_id"), Preserve] public string UserId { get; set; } public override bool Equals(object obj) { if (!(obj is UserPresence item)) { return false; } return Equals(item); } private bool Equals(IUserPresence other) => string.Equals(SessionId, other.SessionId) && string.Equals(UserId, other.UserId); public override int GetHashCode() { unchecked { // ReSharper disable twice NonReadonlyMemberInGetHashCode return ((SessionId?.GetHashCode() ?? 0) * 397) ^ (UserId?.GetHashCode() ?? 0); } } public override string ToString() { return $"UserPresence(Persistence={Persistence}, SessionId='{SessionId}', Status='{Status}', Username='{Username}', UserId='{UserId}')"; } } } ================================================ FILE: Nakama/MatchCreateMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// A create message for a match on the server. /// internal class MatchCreateMessage { [DataMember(Name = "name"), Preserve] public string Name { get; set; } public override string ToString() => $"MatchCreateMessage(name='{Name}')"; } } ================================================ FILE: Nakama/MatchJoinMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// A join message for a match on the server. /// internal class MatchJoinMessage { [DataMember(Name="match_id"), Preserve] public string MatchId { get; set; } [DataMember(Name="token"), Preserve] public string Token { get; set; } [DataMember(Name="metadata"), Preserve] public IDictionary Metadata { get; set; } public override string ToString() { return $"MatchJoinMessage(MatchId='{MatchId}', Token='{Token}, Metadata='{Metadata}')"; } } } ================================================ FILE: Nakama/MatchLeaveMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// A leave message for a match on the server. /// internal class MatchLeaveMessage { [DataMember(Name="match_id"), Preserve] public string MatchId { get; set; } public override string ToString() { return $"MatchLeaveMessage(MatchId='{MatchId}')"; } } } ================================================ FILE: Nakama/MatchSendMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// Send new state to a match on the server. /// internal class MatchSendMessage { [DataMember(Name="match_id"), Preserve] public string MatchId { get; set; } [DataMember(Name="op_code"), Preserve] public string OpCode { get; set; } [DataMember(Name="presences"), Preserve] public List Presences { get; set; } [DataMember(Name="data"), Preserve] public string State { get; set; } public override string ToString() { var presences = string.Join(", ", Presences); return $"MatchSendMessage(MatchId='{MatchId}', OpCode={OpCode}, Presences=[{presences}], State='{State}')"; } } } ================================================ FILE: Nakama/MatchmakerAddMessage.cs ================================================ // Copyright 2018 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// Add the user to the matchmaker pool with properties. /// internal class MatchmakerAddMessage { [DataMember(Name = "max_count"), Preserve] public int MaxCount { get; set; } [DataMember(Name = "min_count"), Preserve] public int MinCount { get; set; } [DataMember(Name = "numeric_properties"), Preserve] public Dictionary NumericProperties { get; set; } [DataMember(Name = "query"), Preserve] public string Query { get; set; } [DataMember(Name = "string_properties"), Preserve] public Dictionary StringProperties { get; set; } [DataMember(Name = "count_multiple"), Preserve] public int? CountMultiple { get; set; } public override string ToString() => $"MatchmakerAddMessage(MaxCount={MaxCount}, MinCount={MinCount}, NumericProperties={NumericProperties}, Query='{Query}', StringProperties={StringProperties}, CountMultiple={CountMultiple})"; } } ================================================ FILE: Nakama/MatchmakerRemoveMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// Remove the user from the matchmaker pool by ticket. /// internal class MatchmakerRemoveMessage { [DataMember(Name="ticket"), Preserve] public string Ticket { get; set; } public override string ToString() { return $"MatchmakerRemoveMessage(Ticket='{Ticket}')"; } } } ================================================ FILE: Nakama/Nakama.csproj ================================================ net46;netstandard2.1 8 true 3.0.0.0 3.0.0.0 3.0.0-dev README.md $([System.String]::new('$(GitTag)').Substring(1)) Nakama Authors & contributors Heroic Labs Nakama is an open-source server designed to power modern games and apps. Features include user accounts, chat, social, matchmaker, realtime multiplayer, and much more. The official client which implements the full API and socket options for Nakama server. It's written in C# with minimal dependencies to support Unity, Xamarin, Godot, XNA, and other engines and frameworks. NakamaClient Apache-2.0 clientsdk;nakama;gameserver;backend;restapi https://github.com/heroiclabs/nakama-dotnet ================================================ FILE: Nakama/Ninja.WebSockets/BufferPool.cs ================================================ using System; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; using Nakama.Ninja.WebSockets.Exceptions; namespace Nakama.Ninja.WebSockets { /// /// This buffer pool is instance thread safe /// Use GetBuffer to get a MemoryStream (with a publically accessible buffer) /// Calling Close on this MemoryStream will clear its internal buffer and return the buffer to the pool for reuse /// MemoryStreams can grow larger than the DEFAULT_BUFFER_SIZE (or whatever you passed in) /// and the underlying buffers will be returned to the pool at their larger sizes /// public class BufferPool : IBufferPool { const int DEFAULT_BUFFER_SIZE = 16384; private readonly ConcurrentStack _bufferPoolStack; private readonly int _bufferSize; public BufferPool() : this(DEFAULT_BUFFER_SIZE) { } public BufferPool(int bufferSize) { _bufferSize = bufferSize; _bufferPoolStack = new ConcurrentStack(); } /// /// This memory stream is not instance thread safe (not to be confused with the BufferPool which is instance thread safe) /// protected class PublicBufferMemoryStream : MemoryStream { private readonly BufferPool _bufferPoolInternal; private byte[] _buffer; private MemoryStream _ms; public PublicBufferMemoryStream(byte[] buffer, BufferPool bufferPool) : base(new byte[0]) { _bufferPoolInternal = bufferPool; _buffer = buffer; _ms = new MemoryStream(buffer, 0, buffer.Length, true, true); } public override long Length => base.Length; public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { return _ms.BeginRead(buffer, offset, count, callback, state); } public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { return _ms.BeginWrite(buffer, offset, count, callback, state); } public override bool CanRead => _ms.CanRead; public override bool CanSeek => _ms.CanSeek; public override bool CanTimeout => _ms.CanTimeout; public override bool CanWrite => _ms.CanWrite; public override int Capacity { get => _ms.Capacity; set => _ms.Capacity = value; } public override void Close() { // clear the buffer - we only need to clear up to the number of bytes we have already written Array.Clear(_buffer, 0, (int)_ms.Position); _ms.Close(); // return the buffer to the pool _bufferPoolInternal.ReturnBuffer(_buffer); } public override Task CopyToAsync(System.IO.Stream destination, int bufferSize, CancellationToken cancellationToken) { return _ms.CopyToAsync(destination, bufferSize, cancellationToken); } public override int EndRead(IAsyncResult asyncResult) { return _ms.EndRead(asyncResult); } public override void EndWrite(IAsyncResult asyncResult) { _ms.EndWrite(asyncResult); } public override void Flush() { _ms.Flush(); } public override Task FlushAsync(CancellationToken cancellationToken) { return _ms.FlushAsync(cancellationToken); } public override byte[] GetBuffer() { return _buffer; } public override long Position { get => _ms.Position; set => _ms.Position = value; } public override int Read(byte[] buffer, int offset, int count) { return _ms.Read(buffer, offset, count); } private void EnlargeBufferIfRequired(int count) { // we cannot fit the data into the existing buffer, time for a new buffer if (count > (_buffer.Length - _ms.Position)) { int position = (int)_ms.Position; // double the buffer size long newSize = (long)_buffer.Length * 2; // make sure the new size is big enough long requiredSize = (long)count + _buffer.Length - position; if (requiredSize > int.MaxValue) { throw new WebSocketBufferOverflowException( $"Tried to create a buffer ({requiredSize:#,##0} bytes) that was larger than the max allowed size ({int.MaxValue:#,##0})"); } if (requiredSize > newSize) { // compute the power of two larger than requiredSize. so 40000 => 65536 long candidateSize = (long)Math.Pow(2, Math.Ceiling(Math.Log(requiredSize) / Math.Log(2))); if (candidateSize > int.MaxValue) { newSize = requiredSize; } else { newSize = candidateSize; } } var newBuffer = new byte[newSize]; Buffer.BlockCopy(_buffer, 0, newBuffer, 0, position); _ms = new MemoryStream(newBuffer, 0, newBuffer.Length, true, true) { Position = position }; _buffer = newBuffer; } } public override void WriteByte(byte value) { EnlargeBufferIfRequired(1); _ms.WriteByte(value); } public override void Write(byte[] buffer, int offset, int count) { EnlargeBufferIfRequired(count); _ms.Write(buffer, offset, count); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { EnlargeBufferIfRequired(count); return _ms.WriteAsync(buffer, offset, count); } public override object InitializeLifetimeService() { return _ms.InitializeLifetimeService(); } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return _ms.ReadAsync(buffer, offset, count, cancellationToken); } public override int ReadByte() { return _ms.ReadByte(); } public override int ReadTimeout { get => _ms.ReadTimeout; set => _ms.ReadTimeout = value; } public override long Seek(long offset, SeekOrigin loc) { return _ms.Seek(offset, loc); } /// /// Note: This will not make the MemoryStream any smaller, only larger /// public override void SetLength(long value) { EnlargeBufferIfRequired((int)value); } public override byte[] ToArray() { // you should never call this return _ms.ToArray(); } public override int WriteTimeout { get => _ms.WriteTimeout; set => _ms.WriteTimeout = value; } #if !NET45 public override bool TryGetBuffer(out ArraySegment buffer) { buffer = new ArraySegment(_buffer, 0, (int)_ms.Position); return true; } #endif public override void WriteTo(System.IO.Stream stream) { _ms.WriteTo(stream); } } /// /// Gets a MemoryStream built from a buffer plucked from a thread safe pool /// The pool grows automatically. /// Closing the memory stream clears the buffer and returns it to the pool /// public MemoryStream GetBuffer() { byte[] buffer; if (!_bufferPoolStack.TryPop(out buffer)) { buffer = new byte[_bufferSize]; } return new PublicBufferMemoryStream(buffer, this); } protected void ReturnBuffer(byte[] buffer) { _bufferPoolStack.Push(buffer); } } } ================================================ FILE: Nakama/Ninja.WebSockets/Exceptions/EntityTooLargeException.cs ================================================ using System; namespace Nakama.Ninja.WebSockets.Exceptions { [Serializable] public class EntityTooLargeException : Exception { public EntityTooLargeException() : base() { } /// /// Http header too large to fit in buffer /// public EntityTooLargeException(string message) : base(message) { } public EntityTooLargeException(string message, Exception inner) : base(message, inner) { } } } ================================================ FILE: Nakama/Ninja.WebSockets/Exceptions/InvalidHttpResponseCodeException.cs ================================================ using System; namespace Nakama.Ninja.WebSockets.Exceptions { [Serializable] public class InvalidHttpResponseCodeException : Exception { public string ResponseCode { get; private set; } public string ResponseHeader { get; private set; } public string ResponseDetails { get; private set; } public InvalidHttpResponseCodeException() : base() { } public InvalidHttpResponseCodeException(string message) : base(message) { } public InvalidHttpResponseCodeException(string responseCode, string responseDetails, string responseHeader) : base(responseCode) { ResponseCode = responseCode; ResponseDetails = responseDetails; ResponseHeader = responseHeader; } public InvalidHttpResponseCodeException(string message, Exception inner) : base(message, inner) { } } } ================================================ FILE: Nakama/Ninja.WebSockets/Exceptions/README.txt ================================================ Make sure that exceptions follow the microsoft standards ================================================ FILE: Nakama/Ninja.WebSockets/Exceptions/SecWebSocketKeyMissingException.cs ================================================ using System; namespace Nakama.Ninja.WebSockets.Exceptions { [Serializable] public class SecWebSocketKeyMissingException : Exception { public SecWebSocketKeyMissingException() : base() { } public SecWebSocketKeyMissingException(string message) : base(message) { } public SecWebSocketKeyMissingException(string message, Exception inner) : base(message, inner) { } } } ================================================ FILE: Nakama/Ninja.WebSockets/Exceptions/ServerListenerSocketException.cs ================================================ using System; namespace Nakama.Ninja.WebSockets.Exceptions { [Serializable] public class ServerListenerSocketException : Exception { public ServerListenerSocketException() : base() { } public ServerListenerSocketException(string message) : base(message) { } public ServerListenerSocketException(string message, Exception inner) : base(message, inner) { } } } ================================================ FILE: Nakama/Ninja.WebSockets/Exceptions/WebSocketBufferOverflowException.cs ================================================ using System; namespace Nakama.Ninja.WebSockets.Exceptions { [Serializable] public class WebSocketBufferOverflowException : Exception { public WebSocketBufferOverflowException() : base() { } public WebSocketBufferOverflowException(string message) : base(message) { } public WebSocketBufferOverflowException(string message, Exception inner) : base(message, inner) { } } } ================================================ FILE: Nakama/Ninja.WebSockets/Exceptions/WebSocketHandshakeFailedException.cs ================================================ using System; namespace Nakama.Ninja.WebSockets.Exceptions { [Serializable] public class WebSocketHandshakeFailedException : Exception { public WebSocketHandshakeFailedException() : base() { } public WebSocketHandshakeFailedException(string message) : base(message) { } public WebSocketHandshakeFailedException(string message, Exception inner) : base(message, inner) { } } } ================================================ FILE: Nakama/Ninja.WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs ================================================ using System; namespace Nakama.Ninja.WebSockets.Exceptions { [Serializable] public class WebSocketVersionNotSupportedException : Exception { public WebSocketVersionNotSupportedException() : base() { } public WebSocketVersionNotSupportedException(string message) : base(message) { } public WebSocketVersionNotSupportedException(string message, Exception inner) : base(message, inner) { } } } ================================================ FILE: Nakama/Ninja.WebSockets/HttpHelper.cs ================================================ // --------------------------------------------------------------------- // Copyright 2018 David Haig // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // --------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Linq; using Nakama.Ninja.WebSockets.Exceptions; namespace Nakama.Ninja.WebSockets { public class HttpHelper { private const string HTTP_GET_HEADER_REGEX = @"^GET(.*)HTTP\/1\.1"; /// /// Calculates a random WebSocket key that can be used to initiate a WebSocket handshake /// /// A random websocket key public static string CalculateWebSocketKey() { // this is not used for cryptography so doing something simple like he code below is op Random rand = new Random((int)DateTime.Now.Ticks); byte[] keyAsBytes = new byte[16]; rand.NextBytes(keyAsBytes); return Convert.ToBase64String(keyAsBytes); } /// /// Computes a WebSocket accept string from a given key /// /// The web socket key to base the accept string on /// A web socket accept string public static string ComputeSocketAcceptString(string secWebSocketKey) { // this is a guid as per the web socket spec const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; string concatenated = secWebSocketKey + webSocketGuid; byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated); // note an instance of SHA1 is not threadsafe so we have to create a new one every time here byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes); string secWebSocketAccept = Convert.ToBase64String(sha1Hash); return secWebSocketAccept; } /// /// Reads an http header as per the HTTP spec /// /// The stream to read UTF8 text from /// The cancellation token /// The HTTP header public static async Task ReadHttpHeaderAsync(System.IO.Stream stream, CancellationToken token) { int length = 1024 * 16; // 16KB buffer more than enough for http header byte[] buffer = new byte[length]; int offset = 0; int bytesRead = 0; do { if (offset >= length) { throw new EntityTooLargeException("Http header message too large to fit in buffer (16KB)"); } bytesRead = await stream.ReadAsync(buffer, offset, length - offset, token); offset += bytesRead; string header = Encoding.UTF8.GetString(buffer, 0, offset); // as per http specification, all headers should end this this if (header.Contains("\r\n\r\n")) { return header; } } while (bytesRead > 0); return string.Empty; } /// /// Decodes the header to detect is this is a web socket upgrade response /// /// The HTTP header /// True if this is an http WebSocket upgrade response public static bool IsWebSocketUpgradeRequest(String header) { Regex getRegex = new Regex(HTTP_GET_HEADER_REGEX, RegexOptions.IgnoreCase); System.Text.RegularExpressions.Match getRegexMatch = getRegex.Match(header); if (getRegexMatch.Success) { // check if this is a web socket upgrade request Regex webSocketUpgradeRegex = new Regex("Upgrade: websocket", RegexOptions.IgnoreCase); System.Text.RegularExpressions.Match webSocketUpgradeRegexMatch = webSocketUpgradeRegex.Match(header); return webSocketUpgradeRegexMatch.Success; } return false; } /// /// Gets the path from the HTTP header /// /// The HTTP header to read /// The path public static string GetPathFromHeader(string httpHeader) { Regex getRegex = new Regex(HTTP_GET_HEADER_REGEX, RegexOptions.IgnoreCase); System.Text.RegularExpressions.Match getRegexMatch = getRegex.Match(httpHeader); if (getRegexMatch.Success) { // extract the path attribute from the first line of the header return getRegexMatch.Groups[1].Value.Trim(); } return null; } public static IList GetSubProtocols(string httpHeader) { Regex regex = new Regex(@"Sec-WebSocket-Protocol:(?.+)", RegexOptions.IgnoreCase); System.Text.RegularExpressions.Match match = regex.Match(httpHeader); if (match.Success) { const int MAX_LEN = 2048; if (match.Length > MAX_LEN) { throw new EntityTooLargeException( $"Sec-WebSocket-Protocol exceeded the maximum of length of {MAX_LEN}"); } // extract a csv list of sub protocols (in order of highest preference first) string csv = match.Groups["protocols"].Value.Trim(); return csv.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()) .ToList(); } return new List(); } /// /// Reads the HTTP response code from the http response string /// /// The response string /// the response code public static string ReadHttpResponseCode(string response) { Regex getRegex = new Regex(@"HTTP\/1\.1 (.*)", RegexOptions.IgnoreCase); System.Text.RegularExpressions.Match getRegexMatch = getRegex.Match(response); if (getRegexMatch.Success) { // extract the path attribute from the first line of the header return getRegexMatch.Groups[1].Value.Trim(); } return null; } /// /// Writes an HTTP response string to the stream /// /// The response (without the new line characters) /// The stream to write to /// The cancellation token public static async Task WriteHttpHeaderAsync(string response, System.IO.Stream stream, CancellationToken token) { response = response.Trim() + "\r\n\r\n"; Byte[] bytes = Encoding.UTF8.GetBytes(response); await stream.WriteAsync(bytes, 0, bytes.Length, token); } } } ================================================ FILE: Nakama/Ninja.WebSockets/IBufferPool.cs ================================================ using System.IO; namespace Nakama.Ninja.WebSockets { public interface IBufferPool { /// /// Gets a MemoryStream built from a buffer plucked from a thread safe pool /// The pool grows automatically. /// Closing the memory stream clears the buffer and returns it to the pool /// MemoryStream GetBuffer(); } } ================================================ FILE: Nakama/Ninja.WebSockets/IPingPongManager.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; namespace Nakama.Ninja.WebSockets { /// /// Ping Pong Manager used to facilitate ping pong WebSocket messages /// interface IPingPongManager { /// /// Raised when a Pong frame is received /// event EventHandler Pong; /// /// Sends a ping frame /// /// The payload (must be 125 bytes of less) /// The cancellation token Task SendPing(ArraySegment payload, CancellationToken cancellation); } } ================================================ FILE: Nakama/Ninja.WebSockets/IWebSocketClientFactory.cs ================================================ using System; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; namespace Nakama.Ninja.WebSockets { /// /// Web socket client factory used to open web socket client connections /// public interface IWebSocketClientFactory { /// /// Connect with default options /// /// The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL) /// The optional cancellation token /// A connected web socket instance Task ConnectAsync(Uri uri, CancellationToken token = default(CancellationToken)); /// /// Connect with options specified /// /// The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL) /// The WebSocket client options /// The optional cancellation token /// A connected web socket instance Task ConnectAsync(Uri uri, WebSocketClientOptions options, CancellationToken token = default(CancellationToken)); /// /// Connect with a stream that has already been opened and HTTP websocket upgrade request sent /// This function will check the handshake response from the server and proceed if successful /// Use this function if you have specific requirements to open a conenction like using special http headers and cookies /// You will have to build your own HTTP websocket upgrade request /// You may not even choose to use TCP/IP and this function will allow you to do that /// /// The full duplex response stream from the server /// The secWebSocketKey you used in the handshake request /// The WebSocket client options /// The optional cancellation token /// Task ConnectAsync(System.IO.Stream responseStream, string secWebSocketKey, WebSocketClientOptions options, CancellationToken token = default(CancellationToken)); } } ================================================ FILE: Nakama/Ninja.WebSockets/IWebSocketServerFactory.cs ================================================ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; namespace Nakama.Ninja.WebSockets { /// /// Web socket server factory used to open web socket server connections /// public interface IWebSocketServerFactory { /// /// Reads a http header information from a stream and decodes the parts relating to the WebSocket protocot upgrade /// /// The network stream /// The optional cancellation token /// Http data read from the stream Task ReadHttpHeaderFromStreamAsync(System.IO.Stream stream, CancellationToken token = default(CancellationToken)); /// /// Accept web socket with default options /// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext /// /// The http context used to initiate this web socket request /// The optional cancellation token /// A connected web socket Task AcceptWebSocketAsync(WebSocketHttpContext context, CancellationToken token = default(CancellationToken)); /// /// Accept web socket with options specified /// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext /// /// The http context used to initiate this web socket request /// The web socket options /// The optional cancellation token /// A connected web socket Task AcceptWebSocketAsync(WebSocketHttpContext context, WebSocketServerOptions options, CancellationToken token = default(CancellationToken)); } } ================================================ FILE: Nakama/Ninja.WebSockets/Internal/BinaryReaderWriter.cs ================================================ // --------------------------------------------------------------------- // Copyright 2018 David Haig // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // --------------------------------------------------------------------- using System; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Nakama.Ninja.WebSockets.Internal { internal class BinaryReaderWriter { public static async Task ReadExactly(int length, System.IO.Stream stream, ArraySegment buffer, CancellationToken cancellationToken) { if (length == 0) { return; } if (buffer.Count < length) { // This will happen if the calling function supplied a buffer that was too small to fit the payload of the websocket frame. // Note that this can happen on the close handshake where the message size can be larger than the regular payload throw new InternalBufferOverflowException($"Unable to read {length} bytes into buffer (offset: {buffer.Offset} size: {buffer.Count}). Use a larger read buffer"); } int offset = 0; do { int bytesRead = await stream.ReadAsync(buffer.Array, buffer.Offset + offset, length - offset, cancellationToken); if (bytesRead == 0) { throw new EndOfStreamException(string.Format("Unexpected end of stream encountered whilst attempting to read {0:#,##0} bytes", length)); } offset += bytesRead; } while (offset < length); return; } public static async Task ReadUShortExactly(System.IO.Stream stream, bool isLittleEndian, ArraySegment buffer, CancellationToken cancellationToken) { await ReadExactly(2, stream, buffer, cancellationToken); if (!isLittleEndian) { Array.Reverse(buffer.Array, buffer.Offset, 2); // big endian } return BitConverter.ToUInt16(buffer.Array, buffer.Offset); } public static async Task ReadULongExactly(System.IO.Stream stream, bool isLittleEndian, ArraySegment buffer, CancellationToken cancellationToken) { await ReadExactly(8, stream, buffer, cancellationToken); if (!isLittleEndian) { Array.Reverse(buffer.Array, buffer.Offset, 8); // big endian } return BitConverter.ToUInt64(buffer.Array, buffer.Offset); } public static async Task ReadLongExactly(System.IO.Stream stream, bool isLittleEndian, ArraySegment buffer, CancellationToken cancellationToken) { await ReadExactly(8, stream, buffer, cancellationToken); if (!isLittleEndian) { Array.Reverse(buffer.Array, buffer.Offset, 8); // big endian } return BitConverter.ToInt64(buffer.Array, buffer.Offset); } public static void WriteInt(int value, System.IO.Stream stream, bool isLittleEndian) { byte[] buffer = BitConverter.GetBytes(value); if (BitConverter.IsLittleEndian && !isLittleEndian) { Array.Reverse(buffer); } stream.Write(buffer, 0, buffer.Length); } public static void WriteULong(ulong value, System.IO.Stream stream, bool isLittleEndian) { byte[] buffer = BitConverter.GetBytes(value); if (BitConverter.IsLittleEndian && ! isLittleEndian) { Array.Reverse(buffer); } stream.Write(buffer, 0, buffer.Length); } public static void WriteLong(long value, System.IO.Stream stream, bool isLittleEndian) { byte[] buffer = BitConverter.GetBytes(value); if (BitConverter.IsLittleEndian && !isLittleEndian) { Array.Reverse(buffer); } stream.Write(buffer, 0, buffer.Length); } public static void WriteUShort(ushort value, System.IO.Stream stream, bool isLittleEndian) { byte[] buffer = BitConverter.GetBytes(value); if (BitConverter.IsLittleEndian && !isLittleEndian) { Array.Reverse(buffer); } stream.Write(buffer, 0, buffer.Length); } } } ================================================ FILE: Nakama/Ninja.WebSockets/Internal/WebSocketFrame.cs ================================================ using System; using System.Net.WebSockets; namespace Nakama.Ninja.WebSockets.Internal { internal class WebSocketFrame { public bool IsFinBitSet { get; private set; } public WebSocketOpCode OpCode { get; private set; } public int Count { get; private set; } public WebSocketCloseStatus? CloseStatus { get; private set; } public string CloseStatusDescription { get; private set; } public ArraySegment MaskKey { get; private set; } public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, int count, ArraySegment maskKey) { IsFinBitSet = isFinBitSet; OpCode = webSocketOpCode; Count = count; MaskKey = maskKey; } public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, int count, WebSocketCloseStatus closeStatus, string closeStatusDescription, ArraySegment maskKey) : this(isFinBitSet, webSocketOpCode, count, maskKey) { CloseStatus = closeStatus; CloseStatusDescription = closeStatusDescription; } } } ================================================ FILE: Nakama/Ninja.WebSockets/Internal/WebSocketFrameCommon.cs ================================================ // --------------------------------------------------------------------- // Copyright 2018 David Haig // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // --------------------------------------------------------------------- using System; namespace Nakama.Ninja.WebSockets.Internal { internal static class WebSocketFrameCommon { public const int MaskKeyLength = 4; /// /// Mutate payload with the mask key /// This is a reversible process /// If you apply this to masked data it will be unmasked and visa versa /// /// The 4 byte mask key /// The payload to mutate public static void ToggleMask(ArraySegment maskKey, ArraySegment payload) { if (maskKey.Count != MaskKeyLength) { throw new Exception($"MaskKey key must be {MaskKeyLength} bytes"); } byte[] buffer = payload.Array; byte[] maskKeyArray = maskKey.Array; int payloadOffset = payload.Offset; int payloadCountPlusOffset = payload.Count + payloadOffset; int maskKeyOffset = maskKey.Offset; // apply the mask key (this is a reversible process so no need to copy the payload) // NOTE: this is a hot function // TODO: make this faster for (int i = payloadOffset; i < payloadCountPlusOffset; i++) { int payloadIndex = i - payloadOffset; // index should start at zero int maskKeyIndex = maskKeyOffset + (payloadIndex % MaskKeyLength); buffer[i] = (Byte)(buffer[i] ^ maskKeyArray[maskKeyIndex]); } } } } ================================================ FILE: Nakama/Ninja.WebSockets/Internal/WebSocketFrameReader.cs ================================================ // --------------------------------------------------------------------- // Copyright 2018 David Haig // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // --------------------------------------------------------------------- using System; using System.IO; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Nakama.Ninja.WebSockets.Internal { /// /// Reads a WebSocket frame /// see http://tools.ietf.org/html/rfc6455 for specification /// internal static class WebSocketFrameReader { private static int CalculateNumBytesToRead(int numBytesLetfToRead, int bufferSize) { if (bufferSize < numBytesLetfToRead) { // the count needs to be a multiple of the mask key return bufferSize - bufferSize % 4; } else { return numBytesLetfToRead; } } /// /// The last read could not be completed because the read buffer was too small. /// We need to continue reading bytes off the stream. /// Not to be confused with a continuation frame /// /// The stream to read from /// The buffer to read into /// The previous partial websocket frame read plus cursor information /// the cancellation token /// A websocket frame public static async Task ReadFromCursorAsync(System.IO.Stream fromStream, ArraySegment intoBuffer, WebSocketReadCursor readCursor, CancellationToken cancellationToken) { var remainingFrame = readCursor.WebSocketFrame; var minCount = CalculateNumBytesToRead(readCursor.NumBytesLeftToRead, intoBuffer.Count); await BinaryReaderWriter.ReadExactly(minCount, fromStream, intoBuffer, cancellationToken); if (remainingFrame.MaskKey.Count > 0) { ArraySegment payloadToMask = new ArraySegment(intoBuffer.Array, intoBuffer.Offset, minCount); WebSocketFrameCommon.ToggleMask(remainingFrame.MaskKey, payloadToMask); } return new WebSocketReadCursor(remainingFrame, minCount, readCursor.NumBytesLeftToRead - minCount); } /// /// Read a WebSocket frame from the stream /// /// The stream to read from /// The buffer to read into /// the cancellation token /// A websocket frame public static async Task ReadAsync(System.IO.Stream fromStream, ArraySegment intoBuffer, CancellationToken cancellationToken) { // allocate a small buffer to read small chunks of data from the stream var smallBuffer = new ArraySegment(new byte[8]); await BinaryReaderWriter.ReadExactly(2, fromStream, smallBuffer, cancellationToken); byte byte1 = smallBuffer.Array[0]; byte byte2 = smallBuffer.Array[1]; // process first byte byte finBitFlag = 0x80; byte opCodeFlag = 0x0F; bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag; WebSocketOpCode opCode = (WebSocketOpCode)(byte1 & opCodeFlag); // read and process second byte byte maskFlag = 0x80; bool isMaskBitSet = (byte2 & maskFlag) == maskFlag; uint len = await ReadLength(byte2, smallBuffer, fromStream, cancellationToken); int count = (int)len; var minCount = CalculateNumBytesToRead(count, intoBuffer.Count); ArraySegment maskKey = new ArraySegment(); try { // use the masking key to decode the data if needed if (isMaskBitSet) { maskKey = new ArraySegment(smallBuffer.Array, 0, WebSocketFrameCommon.MaskKeyLength); await BinaryReaderWriter.ReadExactly(maskKey.Count, fromStream, maskKey, cancellationToken); await BinaryReaderWriter.ReadExactly(minCount, fromStream, intoBuffer, cancellationToken); ArraySegment payloadToMask = new ArraySegment(intoBuffer.Array, intoBuffer.Offset, minCount); WebSocketFrameCommon.ToggleMask(maskKey, payloadToMask); } else { await BinaryReaderWriter.ReadExactly(minCount, fromStream, intoBuffer, cancellationToken); } } catch (InternalBufferOverflowException e) { throw new InternalBufferOverflowException( $"Supplied buffer too small to read {0} bytes from {Enum.GetName(typeof(WebSocketOpCode), opCode)} frame", e); } WebSocketFrame frame; if (opCode == WebSocketOpCode.ConnectionClose) { frame = DecodeCloseFrame(isFinBitSet, opCode, count, intoBuffer, maskKey); } else { // note that by this point the payload will be populated frame = new WebSocketFrame(isFinBitSet, opCode, count, maskKey); } return new WebSocketReadCursor(frame, minCount, count - minCount); } /// /// Extracts close status and close description information from the web socket frame /// private static WebSocketFrame DecodeCloseFrame(bool isFinBitSet, WebSocketOpCode opCode, int count, ArraySegment buffer, ArraySegment maskKey) { WebSocketCloseStatus closeStatus; string closeStatusDescription; if (count >= 2) { Array.Reverse(buffer.Array, buffer.Offset, 2); // network byte order int closeStatusCode = (int)BitConverter.ToUInt16(buffer.Array, buffer.Offset); if (Enum.IsDefined(typeof(WebSocketCloseStatus), closeStatusCode)) { closeStatus = (WebSocketCloseStatus)closeStatusCode; } else { closeStatus = WebSocketCloseStatus.Empty; } int offset = buffer.Offset + 2; int descCount = count - 2; if (descCount > 0) { closeStatusDescription = Encoding.UTF8.GetString(buffer.Array, offset, descCount); } else { closeStatusDescription = null; } } else { closeStatus = WebSocketCloseStatus.Empty; closeStatusDescription = null; } return new WebSocketFrame(isFinBitSet, opCode, count, closeStatus, closeStatusDescription, maskKey); } /// /// Reads the length of the payload according to the contents of byte2 /// private static async Task ReadLength(byte byte2, ArraySegment smallBuffer, System.IO.Stream fromStream, CancellationToken cancellationToken) { byte payloadLenFlag = 0x7F; uint len = (uint)(byte2 & payloadLenFlag); // read a short length or a long length depending on the value of len if (len == 126) { len = await BinaryReaderWriter.ReadUShortExactly(fromStream, false, smallBuffer, cancellationToken); } else if (len == 127) { len = (uint)await BinaryReaderWriter.ReadULongExactly(fromStream, false, smallBuffer, cancellationToken); const uint maxLen = 2147483648; // 2GB - not part of the spec but just a precaution. Send large volumes of data in smaller frames. // protect ourselves against bad data if (len > maxLen || len < 0) { throw new ArgumentOutOfRangeException( $"Payload length out of range. Min 0 max 2GB. Actual {len:#,##0} bytes."); } } return len; } } } ================================================ FILE: Nakama/Ninja.WebSockets/Internal/WebSocketFrameWriter.cs ================================================ // --------------------------------------------------------------------- // Copyright 2018 David Haig // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // --------------------------------------------------------------------- using System.IO; using System; namespace Nakama.Ninja.WebSockets.Internal { // see http://tools.ietf.org/html/rfc6455 for specification // see fragmentation section for sending multi part messages // EXAMPLE: For a text message sent as three fragments, // the first fragment would have an opcode of TextFrame and isLastFrame false, // the second fragment would have an opcode of ContinuationFrame and isLastFrame false, // the third fragment would have an opcode of ContinuationFrame and isLastFrame true. internal static class WebSocketFrameWriter { /// /// This is used for data masking so that web proxies don't cache the data /// Therefore, there are no cryptographic concerns /// private static readonly Random _random; static WebSocketFrameWriter() { _random = new Random((int)DateTime.Now.Ticks); } /// /// No async await stuff here because we are dealing with a memory stream /// /// The web socket opcode /// Array segment to get payload data from /// Stream to write to /// True is this is the last frame in this message (usually true) public static void Write(WebSocketOpCode opCode, ArraySegment fromPayload, MemoryStream toStream, bool isLastFrame, bool isClient) { MemoryStream memoryStream = toStream; byte finBitSetAsByte = isLastFrame ? (byte)0x80 : (byte)0x00; byte byte1 = (byte)(finBitSetAsByte | (byte)opCode); memoryStream.WriteByte(byte1); // NB, set the mask flag if we are constructing a client frame byte maskBitSetAsByte = isClient ? (byte)0x80 : (byte)0x00; // depending on the size of the length we want to write it as a byte, ushort or ulong if (fromPayload.Count < 126) { byte byte2 = (byte)(maskBitSetAsByte | (byte)fromPayload.Count); memoryStream.WriteByte(byte2); } else if (fromPayload.Count <= ushort.MaxValue) { byte byte2 = (byte)(maskBitSetAsByte | 126); memoryStream.WriteByte(byte2); BinaryReaderWriter.WriteUShort((ushort)fromPayload.Count, memoryStream, false); } else { byte byte2 = (byte)(maskBitSetAsByte | 127); memoryStream.WriteByte(byte2); BinaryReaderWriter.WriteULong((ulong)fromPayload.Count, memoryStream, false); } // if we are creating a client frame then we MUST mack the payload as per the spec if (isClient) { byte[] maskKey = new byte[WebSocketFrameCommon.MaskKeyLength]; _random.NextBytes(maskKey); memoryStream.Write(maskKey, 0, maskKey.Length); // mask the payload ArraySegment maskKeyArraySegment = new ArraySegment(maskKey, 0, maskKey.Length); WebSocketFrameCommon.ToggleMask(maskKeyArraySegment, fromPayload); } memoryStream.Write(fromPayload.Array, fromPayload.Offset, fromPayload.Count); } } } ================================================ FILE: Nakama/Ninja.WebSockets/Internal/WebSocketImplementation.cs ================================================ // --------------------------------------------------------------------- // Copyright 2018 David Haig // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // --------------------------------------------------------------------- using System; using System.IO; using System.IO.Compression; using System.Net.WebSockets; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; #if RELEASESIGNED [assembly: InternalsVisibleTo("Ninja.WebSockets.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1707056f4761b7846ed503642fcde97fc350c939f78026211304a56ba51e094c9cefde77fadce5b83c0a621c17f032c37c520b6d9ab2da8291a21472175d9caad55bf67bab4bffb46a96f864ea441cf695edc854296e02a44062245a4e09ccd9a77ef6146ecf941ce1d9da078add54bc2d4008decdac2fa2b388e17794ee6a6")] #else [assembly: InternalsVisibleTo("Ninja.WebSockets.UnitTests")] #endif namespace Nakama.Ninja.WebSockets.Internal { /// /// Main implementation of the WebSocket abstract class /// internal class WebSocketImplementation : WebSocket { private readonly Guid _guid; private readonly Func _recycledStreamFactory; private readonly System.IO.Stream _stream; private readonly bool _includeExceptionInCloseResponse; private readonly bool _isClient; private readonly string _subProtocol; private CancellationTokenSource _internalReadCts; private WebSocketState _state; private readonly IPingPongManager _pingPongManager; private bool _isContinuationFrame; private WebSocketMessageType _continuationFrameMessageType = WebSocketMessageType.Binary; private WebSocketReadCursor _readCursor; private readonly bool _usePerMessageDeflate = false; private bool _tryGetBufferFailureLogged = false; const int MAX_PING_PONG_PAYLOAD_LEN = 125; private WebSocketCloseStatus? _closeStatus; private string _closeStatusDescription; private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); public event EventHandler Pong; internal WebSocketImplementation(Guid guid, Func recycledStreamFactory, System.IO.Stream stream, TimeSpan keepAliveInterval, string secWebSocketExtensions, bool includeExceptionInCloseResponse, bool isClient, string subProtocol) { _guid = guid; _recycledStreamFactory = recycledStreamFactory; _stream = stream; _isClient = isClient; _subProtocol = subProtocol; _internalReadCts = new CancellationTokenSource(); _state = WebSocketState.Open; _readCursor = new WebSocketReadCursor(null, 0, 0); if (secWebSocketExtensions?.IndexOf("permessage-deflate") >= 0) { _usePerMessageDeflate = true; } KeepAliveInterval = keepAliveInterval; _includeExceptionInCloseResponse = includeExceptionInCloseResponse; if (keepAliveInterval.Ticks < 0) { throw new InvalidOperationException("KeepAliveInterval must be Zero or positive"); } if (keepAliveInterval != TimeSpan.Zero) { _pingPongManager = new PingPongManager(guid, this, keepAliveInterval, _internalReadCts.Token); } } public override WebSocketCloseStatus? CloseStatus => _closeStatus; public override string CloseStatusDescription => _closeStatusDescription; public override WebSocketState State { get { return _state; } } public override string SubProtocol => _subProtocol; public TimeSpan KeepAliveInterval { get; private set; } /// /// Receive web socket result /// /// The buffer to copy data into /// The cancellation token /// The web socket result details public override async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) { try { // we may receive control frames so reading needs to happen in an infinite loop while (true) { // allow this operation to be cancelled from iniside OR outside this instance using (CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_internalReadCts.Token, cancellationToken)) { WebSocketFrame frame; try { if (_readCursor.NumBytesLeftToRead > 0) { // If the buffer used to read the frame was too small to fit the whole frame then we need to "remember" this frame // and return what we have. Subsequent calls to the read function will simply continue reading off the stream without // decoding the first few bytes as a websocket header. _readCursor = await WebSocketFrameReader.ReadFromCursorAsync(_stream, buffer, _readCursor, linkedCts.Token); frame = _readCursor.WebSocketFrame; } else { _readCursor = await WebSocketFrameReader.ReadAsync(_stream, buffer, linkedCts.Token); frame = _readCursor.WebSocketFrame; } } catch (InternalBufferOverflowException ex) { await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.MessageTooBig, "Frame too large to fit in buffer. Use message fragmentation", ex); throw; } catch (ArgumentOutOfRangeException ex) { await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.ProtocolError, "Payload length out of range", ex); throw; } catch (EndOfStreamException ex) { await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.InvalidPayloadData, "Unexpected end of stream encountered", ex); throw; } catch (OperationCanceledException ex) { await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Operation cancelled", ex); throw; } catch (Exception ex) { await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.InternalServerError, "Error reading WebSocket frame", ex); throw; } var endOfMessage = frame.IsFinBitSet && _readCursor.NumBytesLeftToRead == 0; switch (frame.OpCode) { case WebSocketOpCode.ConnectionClose: return await RespondToCloseFrame(frame, buffer, linkedCts.Token); case WebSocketOpCode.Ping: ArraySegment pingPayload = new ArraySegment(buffer.Array, buffer.Offset, _readCursor.NumBytesRead); await SendPongAsync(pingPayload, linkedCts.Token); break; case WebSocketOpCode.Pong: ArraySegment pongBuffer = new ArraySegment(buffer.Array, _readCursor.NumBytesRead, buffer.Offset); Pong?.Invoke(this, new PongEventArgs(pongBuffer)); break; case WebSocketOpCode.TextFrame: if (!frame.IsFinBitSet) { // continuation frames will follow, record the message type Text _continuationFrameMessageType = WebSocketMessageType.Text; } return new WebSocketReceiveResult(_readCursor.NumBytesRead, WebSocketMessageType.Text, endOfMessage); case WebSocketOpCode.BinaryFrame: if (!frame.IsFinBitSet) { // continuation frames will follow, record the message type Binary _continuationFrameMessageType = WebSocketMessageType.Binary; } return new WebSocketReceiveResult(_readCursor.NumBytesRead, WebSocketMessageType.Binary, endOfMessage); case WebSocketOpCode.ContinuationFrame: return new WebSocketReceiveResult(_readCursor.NumBytesRead, _continuationFrameMessageType, endOfMessage); default: Exception ex = new NotSupportedException($"Unknown WebSocket opcode {frame.OpCode}"); await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex); throw ex; } } } } catch (Exception catchAll) { // Most exceptions will be caught closer to their source to send an appropriate close message (and set the WebSocketState) // However, if an unhandled exception is encountered and a close message not sent then send one here if (_state == WebSocketState.Open) { await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.InternalServerError, "Unexpected error reading from WebSocket", catchAll); } throw; } } /// /// Send data to the web socket /// /// the buffer containing data to send /// The message type. Can be Text or Binary /// True if this message is a standalone message (this is the norm) /// If it is a multi-part message then false (and true for the last message) /// the cancellation token public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { using (MemoryStream stream = _recycledStreamFactory()) { WebSocketOpCode opCode = GetOppCode(messageType); if (_usePerMessageDeflate) { // NOTE: Compression is currently work in progress and should NOT be used in this library. // The code below is very inefficient for small messages. Ideally we would like to have some sort of moving window // of data to get the best compression. And we don't want to create new buffers which is bad for GC. using (MemoryStream temp = new MemoryStream()) { DeflateStream deflateStream = new DeflateStream(temp, CompressionMode.Compress); deflateStream.Write(buffer.Array, buffer.Offset, buffer.Count); deflateStream.Flush(); var compressedBuffer = new ArraySegment(temp.ToArray()); WebSocketFrameWriter.Write(opCode, compressedBuffer, stream, endOfMessage, _isClient); } } else { WebSocketFrameWriter.Write(opCode, buffer, stream, endOfMessage, _isClient); } await WriteStreamToNetwork(stream, cancellationToken); _isContinuationFrame = !endOfMessage; // TODO: is this correct?? } } /// /// Call this automatically from server side each keepAliveInterval period /// NOTE: ping payload must be 125 bytes or less /// public async Task SendPingAsync(ArraySegment payload, CancellationToken cancellationToken) { if (payload.Count > MAX_PING_PONG_PAYLOAD_LEN) { throw new InvalidOperationException( $"Cannot send Ping: Max ping message size {MAX_PING_PONG_PAYLOAD_LEN} exceeded: {payload.Count}"); } if (_state == WebSocketState.Open) { using (MemoryStream stream = _recycledStreamFactory()) { WebSocketFrameWriter.Write(WebSocketOpCode.Ping, payload, stream, true, _isClient); await WriteStreamToNetwork(stream, cancellationToken); } } } /// /// Aborts the WebSocket without sending a Close frame /// public override void Abort() { _state = WebSocketState.Aborted; _internalReadCts.Cancel(); } /// /// Polite close (use the close handshake) /// public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) { if (_state == WebSocketState.Open) { using (MemoryStream stream = _recycledStreamFactory()) { ArraySegment buffer = BuildClosePayload(closeStatus, statusDescription); WebSocketFrameWriter.Write(WebSocketOpCode.ConnectionClose, buffer, stream, true, _isClient); await WriteStreamToNetwork(stream, cancellationToken); _state = WebSocketState.CloseSent; } } } /// /// Fire and forget close /// public override async Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) { if (_state == WebSocketState.Open) { _state = WebSocketState.Closed; // set this before we write to the network because the write may fail using (MemoryStream stream = _recycledStreamFactory()) { ArraySegment buffer = BuildClosePayload(closeStatus, statusDescription); WebSocketFrameWriter.Write(WebSocketOpCode.ConnectionClose, buffer, stream, true, _isClient); await WriteStreamToNetwork(stream, cancellationToken).ConfigureAwait(false); } } // cancel pending reads _internalReadCts.Cancel(); } /// /// Dispose will send a close frame if the connection is still open /// public override void Dispose() { try { if (_state == WebSocketState.Open) { CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { CloseOutputAsync(WebSocketCloseStatus.EndpointUnavailable, "Service is Disposed", cts.Token) .Wait(); } catch (OperationCanceledException) {} } // cancel pending reads - usually does nothing _internalReadCts.Cancel(); _stream.Close(); } catch { // ignored } } /// /// Called when a Pong frame is received /// /// protected virtual void OnPong(PongEventArgs e) { Pong?.Invoke(this, e); } /// /// As per the spec, write the close status followed by the close reason /// /// The close status /// Optional extra close details /// The payload to sent in the close frame private ArraySegment BuildClosePayload(WebSocketCloseStatus closeStatus, string statusDescription) { byte[] statusBuffer = BitConverter.GetBytes((ushort)closeStatus); Array.Reverse(statusBuffer); // network byte order (big endian) if (statusDescription == null) { return new ArraySegment(statusBuffer); } else { byte[] descBuffer = Encoding.UTF8.GetBytes(statusDescription); byte[] payload = new byte[statusBuffer.Length + descBuffer.Length]; Buffer.BlockCopy(statusBuffer, 0, payload, 0, statusBuffer.Length); Buffer.BlockCopy(descBuffer, 0, payload, statusBuffer.Length, descBuffer.Length); return new ArraySegment(payload); } } /// NOTE: pong payload must be 125 bytes or less /// Pong should contain the same payload as the ping private async Task SendPongAsync(ArraySegment payload, CancellationToken cancellationToken) { // as per websocket spec if (payload.Count > MAX_PING_PONG_PAYLOAD_LEN) { Exception ex = new InvalidOperationException( $"Max ping message size {MAX_PING_PONG_PAYLOAD_LEN} exceeded: {payload.Count}"); await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex); throw ex; } try { if (_state == WebSocketState.Open) { using (MemoryStream stream = _recycledStreamFactory()) { WebSocketFrameWriter.Write(WebSocketOpCode.Pong, payload, stream, true, _isClient); await WriteStreamToNetwork(stream, cancellationToken); } } } catch (Exception ex) { await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Unable to send Pong response", ex); throw; } } /// /// Called when a Close frame is received /// Send a response close frame if applicable /// private async Task RespondToCloseFrame(WebSocketFrame frame, ArraySegment buffer, CancellationToken token) { _closeStatus = frame.CloseStatus; _closeStatusDescription = frame.CloseStatusDescription; if (_state == WebSocketState.CloseSent) { // this is a response to close handshake initiated by this instance _state = WebSocketState.Closed; } else if (_state == WebSocketState.Open) { // do not echo the close payload back to the client, there is no requirement for it in the spec. // However, the same CloseStatus as recieved should be sent back. ArraySegment closePayload = new ArraySegment(new byte[0], 0, 0); _state = WebSocketState.CloseReceived; using (MemoryStream stream = _recycledStreamFactory()) { WebSocketFrameWriter.Write(WebSocketOpCode.ConnectionClose, closePayload, stream, true, _isClient); await WriteStreamToNetwork(stream, token); } } return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Close, frame.IsFinBitSet, frame.CloseStatus, frame.CloseStatusDescription); } /// /// Note that the way in which the stream buffer is accessed can lead to significant performance problems /// You want to avoid a call to stream.ToArray to avoid extra memory allocation /// MemoryStream can be configured to have its internal buffer accessible. /// private ArraySegment GetBuffer(MemoryStream stream) { #if NET45 // NET45 does not have a TryGetBuffer function on Stream if (_tryGetBufferFailureLogged) { return new ArraySegment(stream.ToArray(), 0, (int)stream.Position); } // note that a MemoryStream will throw an UnuthorizedAccessException if the internal buffer is not public. Set publiclyVisible = true try { return new ArraySegment(stream.GetBuffer(), 0, (int)stream.Position); } catch (UnauthorizedAccessException) { _tryGetBufferFailureLogged = true; return new ArraySegment(stream.ToArray(), 0, (int)stream.Position); } #else // Avoid calling ToArray on the MemoryStream because it allocates a new byte array on tha heap // We avoid this by attempting to access the internal memory stream buffer // This works with supported streams like the recyclable memory stream and writable memory streams ArraySegment buffer; if (!stream.TryGetBuffer(out buffer)) { if (!_tryGetBufferFailureLogged) { _tryGetBufferFailureLogged = true; } // internal buffer not suppoted, fall back to ToArray() byte[] array = stream.ToArray(); buffer = new ArraySegment(array, 0, array.Length); } return new ArraySegment(buffer.Array, buffer.Offset, (int)stream.Position); #endif } /// /// Puts data on the wire /// /// The stream to read data from private async Task WriteStreamToNetwork(MemoryStream stream, CancellationToken cancellationToken) { ArraySegment buffer = GetBuffer(stream); await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { await _stream.WriteAsync(buffer.Array, buffer.Offset, buffer.Count, cancellationToken).ConfigureAwait(false); } finally { _semaphore.Release(); } } /// /// Turns a spec websocket frame opcode into a WebSocketMessageType /// private WebSocketOpCode GetOppCode(WebSocketMessageType messageType) { if (_isContinuationFrame) { return WebSocketOpCode.ContinuationFrame; } else { switch (messageType) { case WebSocketMessageType.Binary: return WebSocketOpCode.BinaryFrame; case WebSocketMessageType.Text: return WebSocketOpCode.TextFrame; case WebSocketMessageType.Close: throw new NotSupportedException( "Cannot use Send function to send a close frame. Use Close function."); default: throw new NotSupportedException($"MessageType {messageType} not supported"); } } } /// /// Automatic WebSocket close in response to some invalid data from the remote websocket host /// /// The close status to use /// A description of why we are closing /// The exception (for logging) private async Task CloseOutputAutoTimeoutAsync(WebSocketCloseStatus closeStatus, string statusDescription, Exception ex) { TimeSpan timeSpan = TimeSpan.FromSeconds(5); try { // we may not want to send sensitive information to the client / server if (_includeExceptionInCloseResponse) { statusDescription = statusDescription + "\r\n\r\n" + ex.ToString(); } var autoCancel = new CancellationTokenSource(timeSpan); await CloseOutputAsync(closeStatus, statusDescription, autoCancel.Token); } catch (OperationCanceledException) { // do not throw an exception because that will mask the original exception } catch { // do not throw an exception because that will mask the original exception } } } } ================================================ FILE: Nakama/Ninja.WebSockets/Internal/WebSocketOpCode.cs ================================================ namespace Nakama.Ninja.WebSockets.Internal { internal enum WebSocketOpCode { ContinuationFrame = 0, TextFrame = 1, BinaryFrame = 2, ConnectionClose = 8, Ping = 9, Pong = 10 } } ================================================ FILE: Nakama/Ninja.WebSockets/Internal/WebSocketReadCursor.cs ================================================ namespace Nakama.Ninja.WebSockets.Internal { internal class WebSocketReadCursor { public WebSocketFrame WebSocketFrame { get; private set; } // Number of bytes read in the last read operation public int NumBytesRead { get; private set; } // Number of bytes remaining to read before we are done reading the entire frame public int NumBytesLeftToRead { get; private set; } public WebSocketReadCursor(WebSocketFrame frame, int numBytesRead, int numBytesLeftToRead) { WebSocketFrame = frame; NumBytesRead = numBytesRead; NumBytesLeftToRead = numBytesLeftToRead; } } } ================================================ FILE: Nakama/Ninja.WebSockets/LICENCE ================================================ The MIT License (MIT) Copyright 2018 David Haig Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Nakama/Ninja.WebSockets/PingPongManager.cs ================================================ // --------------------------------------------------------------------- // Copyright 2018 David Haig // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // --------------------------------------------------------------------- using System; using System.Diagnostics; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Nakama.Ninja.WebSockets.Internal; namespace Nakama.Ninja.WebSockets { /// /// Ping Pong Manager used to facilitate ping pong WebSocket messages /// public class PingPongManager : IPingPongManager { private readonly WebSocketImplementation _webSocket; private readonly Guid _guid; private readonly TimeSpan _keepAliveInterval; private readonly Task _pingTask; private readonly CancellationToken _cancellationToken; private Stopwatch _stopwatch; private long _pingSentTicks; /// /// Raised when a Pong frame is received /// public event EventHandler Pong; /// /// Initialises a new instance of the PingPongManager to facilitate ping pong WebSocket messages. /// If you are manually creating an instance of this class then it is advisable to set keepAliveInterval to /// TimeSpan.Zero when you create the WebSocket instance (using a factory) otherwise you may be automatically /// be sending duplicate Ping messages (see keepAliveInterval below) /// /// The web socket used to listen to ping messages and send pong messages /// The time between automatically sending ping messages. /// Set this to TimeSpan.Zero if you with to manually control sending ping messages. /// /// The token used to cancel a pending ping send AND the automatic sending of ping messages /// if keepAliveInterval is positive public PingPongManager(Guid guid, WebSocket webSocket, TimeSpan keepAliveInterval, CancellationToken cancellationToken) { var webSocketImpl = webSocket as WebSocketImplementation; _webSocket = webSocketImpl ?? throw new InvalidCastException( "Cannot cast WebSocket to an instance of WebSocketImplementation. Please use the web socket factories to create a web socket"); _guid = guid; _keepAliveInterval = keepAliveInterval; _cancellationToken = cancellationToken; webSocketImpl.Pong += WebSocketImpl_Pong; _stopwatch = Stopwatch.StartNew(); if (keepAliveInterval == TimeSpan.Zero) { _pingTask = Task.FromResult(0); } else { _pingTask = Task.Run(PingForever, cancellationToken); } } /// /// Sends a ping frame /// /// The payload (must be 125 bytes of less) /// The cancellation token public async Task SendPing(ArraySegment payload, CancellationToken cancellation) { await _webSocket.SendPingAsync(payload, cancellation); } protected virtual void OnPong(PongEventArgs e) { Pong?.Invoke(this, e); } private async Task PingForever() { try { while (!_cancellationToken.IsCancellationRequested) { await Task.Delay(_keepAliveInterval, _cancellationToken); if (_webSocket.State != WebSocketState.Open) { break; } if (_pingSentTicks != 0) { await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, $"No Pong message received in response to a Ping after KeepAliveInterval {_keepAliveInterval}", _cancellationToken); break; } if (!_cancellationToken.IsCancellationRequested) { _pingSentTicks = _stopwatch.Elapsed.Ticks; ArraySegment buffer = new ArraySegment(BitConverter.GetBytes(_pingSentTicks)); await SendPing(buffer, _cancellationToken); } } } catch (OperationCanceledException) { // normal, do nothing } } private void WebSocketImpl_Pong(object sender, PongEventArgs e) { _pingSentTicks = 0; OnPong(e); } } } ================================================ FILE: Nakama/Ninja.WebSockets/PongEventArgs.cs ================================================ using System; namespace Nakama.Ninja.WebSockets { /// /// Pong EventArgs /// public class PongEventArgs : EventArgs { /// /// The data extracted from a Pong WebSocket frame /// public ArraySegment Payload { get; private set; } /// /// Initialises a new instance of the PongEventArgs class /// /// The pong payload must be 125 bytes or less (can be zero bytes) public PongEventArgs(ArraySegment payload) { Payload = payload; } } } ================================================ FILE: Nakama/Ninja.WebSockets/WebSocketClientFactory.cs ================================================ // --------------------------------------------------------------------- // Copyright 2018 David Haig // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // --------------------------------------------------------------------- using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Net.WebSockets; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Nakama.Ninja.WebSockets.Exceptions; using Nakama.Ninja.WebSockets.Internal; namespace Nakama.Ninja.WebSockets { /// /// Web socket client factory used to open web socket client connections /// public class WebSocketClientFactory : IWebSocketClientFactory { private readonly Func _bufferFactory; private readonly IBufferPool _bufferPool; /// /// Initialises a new instance of the WebSocketClientFactory class without caring about internal buffers /// public WebSocketClientFactory() { _bufferPool = new BufferPool(); _bufferFactory = _bufferPool.GetBuffer; } /// /// Initialises a new instance of the WebSocketClientFactory class with control over internal buffer creation /// /// Used to get a memory stream. Feel free to implement your own buffer pool. MemoryStreams will be disposed when no longer needed and can be returned to the pool. public WebSocketClientFactory(Func bufferFactory) { _bufferFactory = bufferFactory; } /// /// Connect with default options /// /// The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL) /// The optional cancellation token /// A connected web socket instance public async Task ConnectAsync(Uri uri, CancellationToken token = default(CancellationToken)) { return await ConnectAsync(uri, new WebSocketClientOptions(), token); } /// /// Connect with options specified /// /// The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL) /// The WebSocket client options /// The optional cancellation token /// A connected web socket instance public async Task ConnectAsync(Uri uri, WebSocketClientOptions options, CancellationToken token = default(CancellationToken)) { Guid guid = Guid.NewGuid(); string host = uri.Host; int port = uri.Port; string uriScheme = uri.Scheme.ToLower(); bool useSsl = uriScheme == "wss" || uriScheme == "https"; System.IO.Stream stream = await GetStream(guid, useSsl, options.NoDelay, host, port, token); return await PerformHandshake(guid, uri, stream, options, token); } /// /// Connect with a stream that has already been opened and HTTP websocket upgrade request sent /// This function will check the handshake response from the server and proceed if successful /// Use this function if you have specific requirements to open a conenction like using special http headers and cookies /// You will have to build your own HTTP websocket upgrade request /// You may not even choose to use TCP/IP and this function will allow you to do that /// /// The full duplex response stream from the server /// The secWebSocketKey you used in the handshake request /// The WebSocket client options /// The optional cancellation token /// public async Task ConnectAsync(System.IO.Stream responseStream, string secWebSocketKey, WebSocketClientOptions options, CancellationToken token = default(CancellationToken)) { Guid guid = Guid.NewGuid(); return await ConnectAsync(guid, responseStream, secWebSocketKey, options.KeepAliveInterval, options.SecWebSocketExtensions, options.IncludeExceptionInCloseResponse, token); } private async Task ConnectAsync(Guid guid, System.IO.Stream responseStream, string secWebSocketKey, TimeSpan keepAliveInterval, string secWebSocketExtensions, bool includeExceptionInCloseResponse, CancellationToken token) { string response = string.Empty; try { response = await HttpHelper.ReadHttpHeaderAsync(responseStream, token); } catch (Exception ex) { throw new WebSocketHandshakeFailedException("Handshake unexpected failure", ex); } ThrowIfInvalidResponseCode(response); ThrowIfInvalidAcceptString(guid, response, secWebSocketKey); string subProtocol = GetSubProtocolFromHeader(response); return new WebSocketImplementation(guid, _bufferFactory, responseStream, keepAliveInterval, secWebSocketExtensions, includeExceptionInCloseResponse, true, subProtocol); } private string GetSubProtocolFromHeader(string response) { // make sure we escape the accept string which could contain special regex characters string regexPattern = "Sec-WebSocket-Protocol: (.*)"; Regex regex = new Regex(regexPattern, RegexOptions.IgnoreCase); System.Text.RegularExpressions.Match match = regex.Match(response); if (match.Success) { return match.Groups[1].Value.Trim(); } return null; } private void ThrowIfInvalidAcceptString(Guid guid, string response, string secWebSocketKey) { // make sure we escape the accept string which could contain special regex characters string regexPattern = "Sec-WebSocket-Accept: (.*)"; Regex regex = new Regex(regexPattern, RegexOptions.IgnoreCase); string actualAcceptString = regex.Match(response).Groups[1].Value.Trim(); // check the accept string string expectedAcceptString = HttpHelper.ComputeSocketAcceptString(secWebSocketKey); if (expectedAcceptString != actualAcceptString) { string warning = string.Format( $"Handshake failed because the accept string from the server '{expectedAcceptString}' was not the expected string '{actualAcceptString}'"); throw new WebSocketHandshakeFailedException(warning); } } private void ThrowIfInvalidResponseCode(string responseHeader) { string responseCode = HttpHelper.ReadHttpResponseCode(responseHeader); if (responseCode == null) { throw new InvalidHttpResponseCodeException(null, null, responseHeader); } if (!responseCode.StartsWith("101 ", StringComparison.InvariantCultureIgnoreCase)) { string[] lines = responseHeader.Split(new string[] { "\r\n" }, StringSplitOptions.None); for (int i = 0; i < lines.Length; i++) { // if there is more to the message than just the header if (string.IsNullOrWhiteSpace(lines[i])) { StringBuilder builder = new StringBuilder(); for (int j = i + 1; j < lines.Length - 1; j++) { builder.AppendLine(lines[j]); } string responseDetails = builder.ToString(); throw new InvalidHttpResponseCodeException(responseCode, responseDetails, responseHeader); } } } } /// /// Override this if you need more fine grained control over the TLS handshake like setting the SslProtocol or adding a client certificate /// protected virtual void TlsAuthenticateAsClient(SslStream sslStream, string host) { sslStream.AuthenticateAsClient(host, null, SslProtocols.Tls12, true); } /// /// Override this if you need more control over how the stream used for the websocket is created. It does not event need to be a TCP stream /// /// For logging purposes only /// Make a secure connection /// Set to true to send a message immediately with the least amount of latency (typical usage for chat) /// The destination host (can be an IP address) /// The destination port /// Used to cancel the request /// A connected and open stream protected virtual async Task GetStream(Guid loggingGuid, bool isSecure, bool noDelay, string host, int port, CancellationToken cancellationToken) { var tcpClient = new TcpClient(AddressFamily.InterNetworkV6); tcpClient.Client.NoDelay = noDelay; tcpClient.Client.DualMode = true; IPAddress ipAddress; if (IPAddress.TryParse(host, out ipAddress)) { await tcpClient.ConnectAsync(ipAddress, port); } else { await tcpClient.ConnectAsync(host, port); } cancellationToken.ThrowIfCancellationRequested(); System.IO.Stream stream = tcpClient.GetStream(); if (isSecure) { SslStream sslStream = new SslStream(stream, false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null); // This will throw an AuthenticationException if the certificate is not valid TlsAuthenticateAsClient(sslStream, host); return sslStream; } else { return stream; } } /// /// Invoked by the RemoteCertificateValidationDelegate /// If you want to ignore certificate errors (for debugging) then return true /// private static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (sslPolicyErrors == SslPolicyErrors.None) { return true; } // Do not allow this client to communicate with unauthenticated servers. return false; } private static string GetAdditionalHeaders(Dictionary additionalHeaders) { if (additionalHeaders == null || additionalHeaders.Count == 0) { return string.Empty; } else { StringBuilder builder = new StringBuilder(); foreach (KeyValuePair pair in additionalHeaders) { builder.Append($"{pair.Key}: {pair.Value}\r\n"); } return builder.ToString(); } } private async Task PerformHandshake(Guid guid, Uri uri, System.IO.Stream stream, WebSocketClientOptions options, CancellationToken token) { Random rand = new Random(); byte[] keyAsBytes = new byte[16]; rand.NextBytes(keyAsBytes); string secWebSocketKey = Convert.ToBase64String(keyAsBytes); string additionalHeaders = GetAdditionalHeaders(options.AdditionalHttpHeaders); string handshakeHttpRequest = $"GET {uri.PathAndQuery} HTTP/1.1\r\n" + $"Host: {uri.Host}:{uri.Port}\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + $"Sec-WebSocket-Key: {secWebSocketKey}\r\n" + $"Origin: http://{uri.Host}:{uri.Port}\r\n" + $"Sec-WebSocket-Protocol: {options.SecWebSocketProtocol}\r\n" + additionalHeaders + "Sec-WebSocket-Version: 13\r\n\r\n"; byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest); stream.Write(httpRequest, 0, httpRequest.Length); return await ConnectAsync(stream, secWebSocketKey, options, token); } } } ================================================ FILE: Nakama/Ninja.WebSockets/WebSocketClientOptions.cs ================================================ using System; using System.Collections.Generic; namespace Nakama.Ninja.WebSockets { /// /// Client WebSocket init options /// public class WebSocketClientOptions { /// /// How often to send ping requests to the Server /// This is done to prevent proxy servers from closing your connection /// The default is TimeSpan.Zero meaning that it is disabled. /// WebSocket servers usually send ping messages so it is not normally necessary for the client to send them (hence the TimeSpan.Zero default) /// You can manually control ping pong messages using the PingPongManager class. /// If you do that it is advisible to set this KeepAliveInterval to zero for the WebSocketClientFactory /// public TimeSpan KeepAliveInterval { get; set; } /// /// Set to true to send a message immediately with the least amount of latency (typical usage for chat) /// This will disable Nagle's algorithm which can cause high tcp latency for small packets sent infrequently /// However, if you are streaming large packets or sending large numbers of small packets frequently it is advisable to set NoDelay to false /// This way data will be bundled into larger packets for better throughput /// public bool NoDelay { get; set; } /// /// Add any additional http headers to this dictionary /// public Dictionary AdditionalHttpHeaders { get; set; } /// /// Include the full exception (with stack trace) in the close response /// when an exception is encountered and the WebSocket connection is closed /// The default is false /// public bool IncludeExceptionInCloseResponse { get; set; } /// /// WebSocket Extensions as an HTTP header value /// public string SecWebSocketExtensions { get; set; } /// /// A comma separated list of sub protocols in preference order (first one being the most preferred) /// The server will return the first supported sub protocol (or none if none are supported) /// Can be null /// public string SecWebSocketProtocol { get; set; } /// /// Initialises a new instance of the WebSocketClientOptions class /// public WebSocketClientOptions() { KeepAliveInterval = TimeSpan.FromSeconds(20); NoDelay = true; AdditionalHttpHeaders = new Dictionary(); IncludeExceptionInCloseResponse = false; SecWebSocketProtocol = null; } } } ================================================ FILE: Nakama/Ninja.WebSockets/WebSocketHttpContext.cs ================================================ using System.Collections.Generic; namespace Nakama.Ninja.WebSockets { /// /// The WebSocket HTTP Context used to initiate a WebSocket handshake /// public class WebSocketHttpContext { /// /// True if this is a valid WebSocket request /// public bool IsWebSocketRequest { get; private set; } public IList WebSocketRequestedProtocols { get; private set; } /// /// The raw http header extracted from the stream /// public string HttpHeader { get; private set; } /// /// The Path extracted from the http header /// public string Path { get; private set; } /// /// The stream AFTER the header has already been read /// public System.IO.Stream Stream { get; private set; } /// /// Initialises a new instance of the WebSocketHttpContext class /// /// True if this is a valid WebSocket request /// The raw http header extracted from the stream /// The Path extracted from the http header /// The stream AFTER the header has already been read public WebSocketHttpContext(bool isWebSocketRequest, IList webSocketRequestedProtocols, string httpHeader, string path, System.IO.Stream stream) { IsWebSocketRequest = isWebSocketRequest; WebSocketRequestedProtocols = webSocketRequestedProtocols; HttpHeader = httpHeader; Path = path; Stream = stream; } } } ================================================ FILE: Nakama/Ninja.WebSockets/WebSocketServerFactory.cs ================================================ // --------------------------------------------------------------------- // Copyright 2018 David Haig // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // --------------------------------------------------------------------- using System; using System.Collections.Generic; using System.IO; using System.Net.WebSockets; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Nakama.Ninja.WebSockets.Exceptions; using Nakama.Ninja.WebSockets.Internal; namespace Nakama.Ninja.WebSockets { /// /// Web socket server factory used to open web socket server connections /// public class WebSocketServerFactory : IWebSocketServerFactory { private readonly Func _bufferFactory; private readonly IBufferPool _bufferPool; /// /// Initialises a new instance of the WebSocketServerFactory class without caring about internal buffers /// public WebSocketServerFactory() { _bufferPool = new BufferPool(); _bufferFactory = _bufferPool.GetBuffer; } /// /// Initialises a new instance of the WebSocketClientFactory class with control over internal buffer creation /// /// Used to get a memory stream. Feel free to implement your own buffer pool. MemoryStreams will be disposed when no longer needed and can be returned to the pool. /// public WebSocketServerFactory(Func bufferFactory) { _bufferFactory = bufferFactory; } /// /// Reads a http header information from a stream and decodes the parts relating to the WebSocket protocot upgrade /// /// The network stream /// The optional cancellation token /// Http data read from the stream public async Task ReadHttpHeaderFromStreamAsync(System.IO.Stream stream, CancellationToken token = default(CancellationToken)) { string header = await HttpHelper.ReadHttpHeaderAsync(stream, token); string path = HttpHelper.GetPathFromHeader(header); bool isWebSocketRequest = HttpHelper.IsWebSocketUpgradeRequest(header); IList subProtocols = HttpHelper.GetSubProtocols(header); return new WebSocketHttpContext(isWebSocketRequest, subProtocols, header, path, stream); } /// /// Accept web socket with default options /// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext /// /// The http context used to initiate this web socket request /// The optional cancellation token /// A connected web socket public async Task AcceptWebSocketAsync(WebSocketHttpContext context, CancellationToken token = default(CancellationToken)) { return await AcceptWebSocketAsync(context, new WebSocketServerOptions(), token); } /// /// Accept web socket with options specified /// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext /// /// The http context used to initiate this web socket request /// The web socket options /// The optional cancellation token /// A connected web socket public async Task AcceptWebSocketAsync(WebSocketHttpContext context, WebSocketServerOptions options, CancellationToken token = default(CancellationToken)) { Guid guid = Guid.NewGuid(); await PerformHandshakeAsync(guid, context.HttpHeader, options.SubProtocol, context.Stream, token); string secWebSocketExtensions = null; return new WebSocketImplementation(guid, _bufferFactory, context.Stream, options.KeepAliveInterval, secWebSocketExtensions, options.IncludeExceptionInCloseResponse, false, options.SubProtocol); } private static void CheckWebSocketVersion(string httpHeader) { Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)", RegexOptions.IgnoreCase); // check the version. Support version 13 and above const int WebSocketVersion = 13; System.Text.RegularExpressions.Match match = webSocketVersionRegex.Match(httpHeader); if (match.Success) { int secWebSocketVersion = Convert.ToInt32(match.Groups[1].Value.Trim()); if (secWebSocketVersion < WebSocketVersion) { throw new WebSocketVersionNotSupportedException( $"WebSocket Version {secWebSocketVersion} not suported. Must be {WebSocketVersion} or above"); } } else { throw new WebSocketVersionNotSupportedException("Cannot find \"Sec-WebSocket-Version\" in http header"); } } private static async Task PerformHandshakeAsync(Guid guid, String httpHeader, string subProtocol, System.IO.Stream stream, CancellationToken token) { try { Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)", RegexOptions.IgnoreCase); CheckWebSocketVersion(httpHeader); System.Text.RegularExpressions.Match match = webSocketKeyRegex.Match(httpHeader); if (match.Success) { string secWebSocketKey = match.Groups[1].Value.Trim(); string setWebSocketAccept = HttpHelper.ComputeSocketAcceptString(secWebSocketKey); string response = ("HTTP/1.1 101 Switching Protocols\r\n" + "Connection: Upgrade\r\n" + "Upgrade: websocket\r\n" + (subProtocol != null ? $"Sec-WebSocket-Protocol: {subProtocol}\r\n" : "") + $"Sec-WebSocket-Accept: {setWebSocketAccept}"); await HttpHelper.WriteHttpHeaderAsync(response, stream, token); } else { throw new SecWebSocketKeyMissingException("Unable to read \"Sec-WebSocket-Key\" from http header"); } } catch (WebSocketVersionNotSupportedException ex) { string response = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13" + ex.Message; await HttpHelper.WriteHttpHeaderAsync(response, stream, token); throw; } catch (Exception) { await HttpHelper.WriteHttpHeaderAsync("HTTP/1.1 400 Bad Request", stream, token); throw; } } } } ================================================ FILE: Nakama/Ninja.WebSockets/WebSocketServerOptions.cs ================================================ using System; namespace Nakama.Ninja.WebSockets { /// /// Server WebSocket init options /// public class WebSocketServerOptions { /// /// How often to send ping requests to the Client /// The default is 60 seconds /// This is done to prevent proxy servers from closing your connection /// A timespan of zero will disable the automatic ping pong mechanism /// You can manually control ping pong messages using the PingPongManager class. /// If you do that it is advisible to set this KeepAliveInterval to zero in the WebSocketServerFactory /// public TimeSpan KeepAliveInterval { get; set; } /// /// Include the full exception (with stack trace) in the close response /// when an exception is encountered and the WebSocket connection is closed /// The default is false /// public bool IncludeExceptionInCloseResponse { get; set; } /// /// Specifies the sub protocol to send back to the client in the opening handshake /// Can be null (the most common use case) /// The client can specify multiple preferred protocols in the opening handshake header /// The server should use the first supported one or set this to null if none of the requested sub protocols are supported /// public string SubProtocol { get; set; } /// /// Initialises a new instance of the WebSocketServerOptions class /// public WebSocketServerOptions() { KeepAliveInterval = TimeSpan.FromSeconds(60); IncludeExceptionInCloseResponse = false; SubProtocol = null; } } } ================================================ FILE: Nakama/NullLogger.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Nakama { /// /// A logger which writes to nowhere. /// internal class NullLogger : ILogger { public static readonly ILogger Instance = new NullLogger(); private NullLogger() { } /// public void DebugFormat(string format, params object[] args) { } /// public void ErrorFormat(string format, params object[] args) { } /// public void InfoFormat(string format, params object[] args) { } /// public void WarnFormat(string format, params object[] args) { } } } ================================================ FILE: Nakama/Party.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// Incoming information about a party. /// internal class Party : IParty { [DataMember(Name = "party_id"), Preserve] public string Id { get; set; } [DataMember(Name = "open"), Preserve] public bool Open { get; set; } [DataMember(Name = "hidden"), Preserve] public bool Hidden { get; set; } [DataMember(Name = "max_size"), Preserve] public int MaxSize { get; set; } [DataMember(Name = "label"), Preserve] public string Label { get; set; } public IUserPresence Self => SelfField; [DataMember(Name = "self"), Preserve] public UserPresence SelfField { get; set; } public IUserPresence Leader => LeaderField; [DataMember(Name = "leader"), Preserve] public UserPresence LeaderField { get; set; } public IEnumerable Presences => PresencesField ?? UserPresence.NoPresences; [DataMember(Name = "presences"), Preserve] public List PresencesField { get; set; } public void UpdatePresences(IPartyPresenceEvent presenceEvent) { if (presenceEvent.PartyId != Id) { throw new InvalidOperationException("Tried updating presences belonging to the wrong party."); } PresencesField = PresenceUtil.CopyJoinsAndLeaves(PresencesField, presenceEvent.Joins, presenceEvent.Leaves); } } } ================================================ FILE: Nakama/PartyAccept.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { // Accept a request to join. internal class PartyAccept { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } [DataMember(Name = "presence"), Preserve] public UserPresence Presence { get; set; } public override string ToString() => $"PartyAccept(PartyId='{PartyId}', Presence={Presence})"; } } ================================================ FILE: Nakama/PartyClose.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// End a party, kicking all party members, and closing it. /// internal class PartyClose : IPartyClose { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } public override string ToString() => $"PartyClose(PartyId='{PartyId}')"; } } ================================================ FILE: Nakama/PartyCreate.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Create a new party. /// internal class PartyCreate { [DataMember(Name = "open"), Preserve] public bool Open { get; set; } [DataMember(Name = "hidden"), Preserve] public bool Hidden { get; set; } [DataMember(Name = "max_size"), Preserve] public int MaxSize { get; set; } [DataMember(Name = "label"), Preserve] public string Label { get; set; } public override string ToString() => $"PartyCreate(Open={Open}, MaxSize={MaxSize}, Label={Label})"; } } ================================================ FILE: Nakama/PartyData.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Runtime.Serialization; namespace Nakama { /// /// Incoming party data delivered from the server. /// internal class PartyData : IPartyData { private static readonly byte[] NoBytes = new byte[0]; [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } public IUserPresence Presence => PresenceField; [DataMember(Name = "presence"), Preserve] public UserPresence PresenceField { get; set; } public long OpCode => Convert.ToInt64(OpCodeField); [DataMember(Name = "op_code"), Preserve] public string OpCodeField { get; set; } public byte[] Data => DataField == null ? NoBytes : Convert.FromBase64String(DataField); [DataMember(Name = "data"), Preserve] public string DataField { get; set; } public override string ToString() => $"PartyData(PartyId='{PartyId}', Presence={Presence}, OpCode={OpCode}, Data={Data})"; } } ================================================ FILE: Nakama/PartyDataSend.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Send data to a party. /// internal class PartyDataSend { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } [DataMember(Name = "op_code"), Preserve] public string OpCode { get; set; } [DataMember(Name = "data"), Preserve] public string Data { get; set; } public override string ToString() => $"PartyDataSend(PartyId='{PartyId}', OpCode={OpCode}, Data='{Data}')"; } } ================================================ FILE: Nakama/PartyJoin.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Join a party, or request to join if the party is not open. /// internal class PartyJoin { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } public override string ToString() => $"PartyJoin(PartyId='{PartyId}')"; } } ================================================ FILE: Nakama/PartyJoinRequest.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// Incoming notification for one or more new presences attempting to join the party. /// internal class PartyJoinRequest : IPartyJoinRequest { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } public IEnumerable Presences => PresencesField ?? UserPresence.NoPresences; [DataMember(Name = "presences"), Preserve] public List PresencesField { get; set; } public override string ToString() => $"PartyJoinRequest(PartyId='{PartyId}', Presences={string.Join(", ", Presences)})"; } } ================================================ FILE: Nakama/PartyJoinRequestList.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Request a list of pending join requests for a party. /// internal class PartyJoinRequestList { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } public override string ToString() => $"PartyJoinRequestList(PartyId='{PartyId}')"; } } ================================================ FILE: Nakama/PartyLeader.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Announcement of a new party leader. /// internal class PartyLeader : IPartyLeader { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } public IUserPresence Presence => PresenceField; [DataMember(Name = "presence"), Preserve] public UserPresence PresenceField { get; set; } public override string ToString() => $"PartyLeader(PartyId='{PartyId}', Presence={Presence})"; } } ================================================ FILE: Nakama/PartyLeave.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Leave a party. /// internal class PartyLeave { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } public override string ToString() => $"PartyLeave(PartyId='{PartyId}')"; } } ================================================ FILE: Nakama/PartyMatchmakerAdd.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// Begin matchmaking as a party. /// internal class PartyMatchmakerAdd { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } [DataMember(Name = "max_count"), Preserve] public int MaxCount { get; set; } [DataMember(Name = "min_count"), Preserve] public int MinCount { get; set; } [DataMember(Name = "query"), Preserve] public string Query { get; set; } [DataMember(Name = "string_properties"), Preserve] public Dictionary StringProperties { get; set; } [DataMember(Name = "numeric_properties"), Preserve] public Dictionary NumericProperties { get; set; } [DataMember(Name = "count_multiple"), Preserve] public int? CountMultiple { get; set; } public override string ToString() => $"PartyMatchmakerAdd(PartyId='{PartyId}', MaxCount={MaxCount}, MinCount={MinCount}, NumericProperties={NumericProperties}, Query='{Query}', StringProperties={StringProperties}, CountMultiple={CountMultiple})"; } } ================================================ FILE: Nakama/PartyMatchmakerRemove.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Remove the party from the matchmaker. /// internal class PartyMatchmakerRemove { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } [DataMember(Name = "ticket"), Preserve] public string Ticket { get; set; } public override string ToString() => $"PartyMatchmakerRemove(PartyId='{PartyId}', Ticket='{Ticket}')"; } } ================================================ FILE: Nakama/PartyMatchmakerTicket.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// internal class PartyMatchmakerTicket : IPartyMatchmakerTicket { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } [DataMember(Name = "ticket"), Preserve] public string Ticket { get; set; } public override string ToString() => $"PartyMatchmakerTicket(PartyId='{PartyId}', Ticket='{Ticket}')"; } } ================================================ FILE: Nakama/PartyMemberRemove.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Kick a party member, or decline a request to join. /// internal class PartyMemberRemove { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } [DataMember(Name = "presence"), Preserve] public UserPresence Presence { get; set; } public override string ToString() => $"PartyMemberRemove(PartyId='{PartyId}', Presence={Presence})"; } } ================================================ FILE: Nakama/PartyPresenceEvent.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; using System.Runtime.Serialization; using static System.String; namespace Nakama { /// internal class PartyPresenceEvent : IPartyPresenceEvent { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } public IEnumerable Leaves => LeavesField ?? UserPresence.NoPresences; [DataMember(Name = "leaves"), Preserve] public List LeavesField { get; set; } public IEnumerable Joins => JoinsField ?? UserPresence.NoPresences; [DataMember(Name = "joins"), Preserve] public List JoinsField { get; set; } public override string ToString() => $"PartyPresenceEvent(PartyId='{PartyId}', Leaves=[{Join(", ", Leaves)}], Joins=[{Join(", ", Joins)}])"; } } ================================================ FILE: Nakama/PartyPromote.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Promote a new party leader. /// internal class PartyPromote { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } [DataMember(Name = "presence"), Preserve] public UserPresence Presence { get; set; } public override string ToString() => $"PartyPromote(PartyId='{PartyId}', Presence={Presence})"; } } ================================================ FILE: Nakama/PartyUpdate.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Runtime.Serialization; namespace Nakama { /// /// Create a new party. /// internal class PartyUpdate : IPartyUpdate { [DataMember(Name = "party_id"), Preserve] public string PartyId { get; set; } [DataMember(Name = "open"), Preserve] public bool Open { get; set; } [DataMember(Name = "hidden"), Preserve] public bool Hidden { get; set; } [DataMember(Name = "label"), Preserve] public string Label { get; set; } public override string ToString() => $"PartyUpdate(PartyId={PartyId}, Open={Open}, Label={Label})"; } } ================================================ FILE: Nakama/PresenceUtil.cs ================================================ // Copyright 2023 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; namespace Nakama { internal static class PresenceUtil { /// /// Applies joins and leaves of a presence event to a copy of the provided presence list and returns the result. /// public static List CopyJoinsAndLeaves(List currentPresences, IEnumerable joins, IEnumerable leaves) { currentPresences = currentPresences ?? new List(); joins = joins ?? new List(); leaves = leaves ?? new List(); var newPresences = new Dictionary(); foreach (UserPresence presence in currentPresences) { newPresences[presence.UserId] = presence; } foreach (IUserPresence join in joins) { if (newPresences.ContainsKey(join.UserId)) { // unexpected continue; } newPresences.Add(join.UserId, join as UserPresence ?? IUserPresenceToUserPresence(join)); } foreach (IUserPresence leave in leaves) { if (!newPresences.ContainsKey(leave.UserId)) { // unexpected continue; } newPresences.Remove(leave.UserId); } return new List(newPresences.Values); } private static UserPresence IUserPresenceToUserPresence(IUserPresence userPresence) { return new UserPresence { Persistence = userPresence.Persistence, SessionId = userPresence.SessionId, Status = userPresence.Status, Username = userPresence.Username, UserId = userPresence.UserId }; } } } ================================================ FILE: Nakama/PreserveAttribute.cs ================================================ /* * Copyright 2020 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama { /// /// A custom attribute recognized by Unity3D. When added to a class member, it prevents /// the Unity linker from stripping the code it is associated with. This is used in addition /// to the link.xml file because the Unity Package Manager does not recognize link.xml files /// inside Unity packages. /// https://docs.unity3d.com/2018.3/Documentation/Manual/ManagedCodeStripping.html /// internal class PreserveAttribute : System.Attribute { } } ================================================ FILE: Nakama/Retry.cs ================================================ /* * Copyright 2021 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama { /// /// Represents a single retry attempt. /// public class Retry { /// /// The delay (milliseconds) in the request retry attributable to the exponential backoff algorithm. /// public int ExponentialBackoff { get; } /// /// The delay (milliseconds) in the request retry attributable to the jitter algorithm. /// public int JitterBackoff { get; } internal Retry(int expoBackoff, int jitterBackoff) { ExponentialBackoff = expoBackoff; JitterBackoff = jitterBackoff; } } } ================================================ FILE: Nakama/RetryConfiguration.cs ================================================ /* * Copyright 2021 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama { /// /// A configuration for controlling retriable requests. /// /// /// Retry configurations can be assigned to the on a request-by-request basis via /// the see parameter. /// /// Retry configurations can also be assigned on a global basis using . /// Configurations passed via the see parameter take precedence over the global configuration. /// public class RetryConfiguration { /// /// The base delay (milliseconds) used to calculate the time before making another request attempt. /// This base will be raised to N, where N is the number of retry attempts. /// public int BaseDelayMs { get; } /// /// The jitter algorithm used to apply randomness to the retry delay. Defaults to /// public Jitter Jitter { get; } /// /// The maximum number of attempts to make before cancelling the request task. /// public int MaxAttempts { get; } /// /// A callback that is invoked before a new retry attempt is made. /// public RetryListener RetryListener { get; } /// /// Create a new retry configuration. /// /// The base delay (milliseconds) used to calculate the time before making another request attempt. /// The maximum number of attempts to make before cancelling the request task. public RetryConfiguration(int baseDelayMs, int maxRetries) : this(baseDelayMs, maxRetries, null, RetryJitter.FullJitter) {} /// /// Create a new retry configuration. /// /// The base delay (milliseconds) used to calculate the time before making another request attempt. /// The maximum number of attempts to make before cancelling the request task. /// A callback that is invoked before a new retry attempt is made. public RetryConfiguration(int baseDelayMs, int maxRetries, RetryListener listener) : this(baseDelayMs, maxRetries, listener, RetryJitter.FullJitter) {} /// /// Create a new retry configuration. /// /// The base delay (milliseconds) used to calculate the time before making another request attempt. /// The maximum number of attempts to make before cancelling the request task. /// A callback that is invoked before a new retry attempt is made. /// The jitter algorithm used to apply randomness to the retry delay. public RetryConfiguration(int baseDelayMs, int maxRetries, RetryListener listener, Jitter jitter) { BaseDelayMs = baseDelayMs; RetryListener = listener; MaxAttempts = maxRetries; Jitter = jitter; } } } ================================================ FILE: Nakama/RetryHistory.cs ================================================ /* * Copyright 2021 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Threading; namespace Nakama { internal class RetryHistory { public RetryConfiguration Configuration { get; } public List Retries { get; } public CancellationToken? UserCancelToken { get; } public Random Random { get; } public RetryHistory(ISession session, RetryConfiguration configuration, CancellationToken? userCancelToken) : this(session.AuthToken, configuration, userCancelToken) { } public RetryHistory(string jitterHashKey, RetryConfiguration configuration, CancellationToken? userCancelToken) { Configuration = configuration; Retries = new List(); UserCancelToken = userCancelToken; Random = new Random(jitterHashKey.GetHashCode()); } } } ================================================ FILE: Nakama/RetryInvoker.cs ================================================ /* * Copyright 2021 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Threading.Tasks; namespace Nakama { /// /// Invokes requests with retry and exponential backoff. /// internal class RetryInvoker { private readonly TransientExceptionDelegate _del; public RetryInvoker(TransientExceptionDelegate del) { if (del == null) { throw new ArgumentException("Cannot initialize retry invoker with a null transient exception delegate."); } _del = del; } public async Task InvokeWithRetry(Func> request, RetryHistory history) { try { return await request(); } catch (Exception e) { if (history.Configuration != null && _del(e)) { await Backoff(history, e); return await InvokeWithRetry(request, history); } else { throw; } } } public async Task InvokeWithRetry(Func request, RetryHistory history) { try { await request(); } catch (Exception e) { if (history.Configuration != null && _del(e)) { await Backoff(history, e); await InvokeWithRetry(request, history); } else { throw; } } } private Retry CreateNewRetry(RetryHistory history) { int expoBackoff = System.Convert.ToInt32(Math.Pow(2, history.Retries.Count)) * history.Configuration.BaseDelayMs; int jitteredBackoff = history.Configuration.Jitter(history.Retries, expoBackoff, history.Random); return new Retry(expoBackoff, jitteredBackoff); } private Task Backoff(RetryHistory history, Exception e) { if (history.Retries.Count >= history.Configuration.MaxAttempts) { throw new TaskCanceledException("Exceeded max retry attempts.", e); } Retry newRetry = CreateNewRetry(history); history.Retries.Add(newRetry); history.Configuration.RetryListener?.Invoke(history.Retries.Count, newRetry); if (history.UserCancelToken.HasValue) { return Task.Delay(newRetry.JitterBackoff, history.UserCancelToken.Value); } else { return Task.Delay(newRetry.JitterBackoff); } } } } ================================================ FILE: Nakama/RetryJitter.cs ================================================ /* * Copyright 2021 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; namespace Nakama { /// /// The Jitter algorithm is responsible for introducing randomness to a delay before a retry. /// /// Information about previous retry attempts. /// A delay (milliseconds) between the last failed attempt in the retry history /// and the next upcoming attempt. /// A object that has been seeded by . /// A new delay (milliseconds) between the last failed attempt in the retry history and the next upcoming attempt. public delegate int Jitter(IList retryHistory, int retryDelay, Random random); /// /// A collection of algorithms. /// public static class RetryJitter { /// /// FullJitter is a Jitter algorithm that selects a random point between now and the next retry time. /// public static int FullJitter(IList retries, int retryDelay, Random random) { return System.Convert.ToInt32(retryDelay * random.NextDouble()); } } } ================================================ FILE: Nakama/RetryListener.cs ================================================ /* * Copyright 2021 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama { /// /// Listens to retry events for a particular request. /// /// The number of retries made so far, including this retry. /// An holding inromation about the retry attempt. public delegate void RetryListener(int numRetry, Retry retry); } ================================================ FILE: Nakama/Session.cs ================================================ // Copyright 2019 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using Nakama.TinyJson; namespace Nakama { /// public class Session : ISession { public static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// public string AuthToken { get; private set; } /// public bool Created { get; } /// public long CreateTime { get; private set; } /// public long ExpireTime { get; private set; } /// public bool IsExpired => HasExpired(DateTime.UtcNow); /// public bool IsRefreshExpired => HasRefreshExpired(DateTime.UtcNow); /// public long RefreshExpireTime { get; private set; } /// public string RefreshToken { get; private set; } /// public IDictionary Vars { get; } /// public string Username { get; private set; } /// public string UserId { get; private set; } /// public bool HasExpired(DateTime offset) { var expireDateTime = Epoch + TimeSpan.FromSeconds(ExpireTime); return offset > expireDateTime; } /// public bool HasRefreshExpired(DateTime offset) { var expireDateTime = Epoch + TimeSpan.FromSeconds(RefreshExpireTime); return offset > expireDateTime; } public override string ToString() { var variables = "{"; foreach (var variable in Vars) { variables = string.Concat(variables, " '", variable.Key, "': '", variable.Value, "', "); } variables = string.Concat(variables, "}"); return $"Session(AuthToken='{AuthToken}', Created={Created}, CreateTime={CreateTime}, ExpireTime={ExpireTime}, RefreshToken={RefreshToken}, RefreshExpireTime={RefreshExpireTime}, Variables={variables}, Username='{Username}', UserId='{UserId}')"; } internal Session(string authToken, string refreshToken, bool created) { Created = created; CreateTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); RefreshExpireTime = 0L; Vars = new Dictionary(); Update(authToken, refreshToken); } /// /// Update the current session token with a new authorization token and refresh token. /// /// The authorization token to update into the session. /// The refresh token to update into the session. public void Update(string authToken, string refreshToken) { AuthToken = authToken; RefreshToken = refreshToken; var json = JwtUnpack(authToken); var decoded = json.FromJson>(); ExpireTime = Convert.ToInt64(decoded["exp"]); Username = decoded["usn"].ToString(); UserId = decoded["uid"].ToString(); if (decoded.ContainsKey("vrs") && decoded["vrs"] is Dictionary dictionary) { foreach (var variable in dictionary) { Vars[variable.Key] = variable.Value.ToString(); } } if (decoded.ContainsKey("iat")) { CreateTime = Convert.ToInt64(decoded["iat"]); } // Check in case clients have not updated to use refresh tokens yet. if (!string.IsNullOrEmpty(refreshToken)) { var json2 = JwtUnpack(refreshToken); var decoded2 = json2.FromJson>(); RefreshExpireTime = Convert.ToInt64(decoded2["exp"]); } } /// /// Restore a session from the auth token. /// /// /// A null or empty authentication token will return null. /// /// The authorization token to restore as a session. /// The refresh token for the session. /// A session. public static ISession Restore(string authToken, string refreshToken = null) { return string.IsNullOrEmpty(authToken) ? null : new Session(authToken, refreshToken, false); } private static string JwtUnpack(string jwt) { // Hack decode JSON payload from JWT. var payload = jwt.Split('.')[1]; var padLength = Math.Ceiling(payload.Length / 4.0) * 4; payload = payload.PadRight(Convert.ToInt32(padLength), '=').Replace('-', '+').Replace('_', '/'); return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payload)); } } } ================================================ FILE: Nakama/Socket.cs ================================================ // Copyright 2019 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Nakama.TinyJson; namespace Nakama { /// /// A socket which implements the Nakama realtime API. /// public class Socket : ISocket { private int _cid; // callback id. /// /// The default timeout for when the socket connects. /// public const int DefaultConnectTimeout = 30; /// /// The default timeout for when the socket sends a message. /// public const int DefaultSendTimeout = 10; /// public event Action Closed; /// public event Action Connected; /// public event Action ReceivedChannelMessage; /// public event Action ReceivedChannelPresence; /// public event Action ReceivedError; /// public event Action ReceivedMatchmakerMatched; /// public event Action ReceivedMatchState; /// public event Action ReceivedMatchPresence; /// public event Action ReceivedNotification; /// public event Action ReceivedStatusPresence; /// public event Action ReceivedStreamPresence; /// public event Action ReceivedStreamState; /// public event Action ReceivedParty; /// public event Action ReceivedPartyClose; /// public event Action ReceivedPartyData; /// public event Action ReceivedPartyUpdate; /// public event Action ReceivedPartyJoinRequest; /// public event Action ReceivedPartyLeader; /// public event Action ReceivedPartyPresence; /// public event Action ReceivedPartyMatchmakerTicket; /// public bool IsConnected => _adapter.IsConnected; /// public bool IsConnecting => _adapter.IsConnecting; /// /// The logger to use with the socket. /// public ILogger Logger { get; set; } private readonly ISocketAdapter _adapter; private readonly Uri _baseUri; private readonly Dictionary> _responses; private readonly TimeSpan _sendTimeoutSec; private readonly object _responsesLock = new object(); /// /// A new socket with default options. /// public Socket() : this(Client.DefaultScheme, Client.DefaultHost, Client.DefaultPort, new WebSocketStdlibAdapter()) { } /// /// A new socket with an adapter. /// /// The adapter for use with the socket. public Socket(ISocketAdapter adapter) : this(Client.DefaultScheme, Client.DefaultHost, Client.DefaultPort, adapter) { } /// /// A new socket with server connection and adapter options. /// /// The protocol scheme. Must be "ws" or "wss". /// The host address of the server. /// The port number of the server. /// The adapter for use with the socket. /// The maximum time allowed for a message to be sent. public Socket(string scheme, string host, int port, ISocketAdapter adapter, int sendTimeoutSec = DefaultSendTimeout) { Logger = NullLogger.Instance; _adapter = adapter; _baseUri = new UriBuilder(scheme, host, port).Uri; _responses = new Dictionary>(); _sendTimeoutSec = TimeSpan.FromSeconds(sendTimeoutSec); _adapter.Connected += () => Connected?.Invoke(); _adapter.Closed += (reason) => { lock (_responsesLock) { foreach (var response in _responses) { response.Value.TrySetCanceled(); } _responses.Clear(); } Closed?.Invoke(reason); }; _adapter.ReceivedError += e => { if (!_adapter.IsConnected) { lock (_responsesLock) { foreach (var response in _responses) { response.Value.TrySetCanceled(); } _responses.Clear(); } } ReceivedError?.Invoke(e); }; _adapter.Received += ProcessMessage; } /// public Task AcceptPartyMemberAsync(string partyId, IUserPresence presence) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyAccept = new PartyAccept { PartyId = partyId, Presence = presence as UserPresence // TODO serialize interface directly in protobuf } }; return SendAsync(envelope); } /// public async Task AddMatchmakerAsync(string query = "*", int minCount = 2, int maxCount = 8, Dictionary stringProperties = null, Dictionary numericProperties = null, int? countMultiple = null) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", MatchmakerAdd = new MatchmakerAddMessage { Query = query, MinCount = minCount, MaxCount = maxCount, StringProperties = stringProperties, NumericProperties = numericProperties, CountMultiple = countMultiple } }; var response = await SendAsync(envelope); return response.MatchmakerTicket; } /// public async Task AddMatchmakerPartyAsync(string partyId, string query, int minCount, int maxCount, Dictionary stringProperties = null, Dictionary numericProperties = null, int? countMultiple = null) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyMatchmakerAdd = new PartyMatchmakerAdd { PartyId = partyId, Query = query, MinCount = minCount, MaxCount = maxCount, StringProperties = stringProperties, NumericProperties = numericProperties, CountMultiple = countMultiple } }; var response = await SendAsync(envelope); return response.PartyMatchmakerTicket; } /// public Task CloseAsync() => _adapter.CloseAsync(); /// public Task ConnectAsync(ISession session, bool appearOnline = false, int connectTimeoutSec = DefaultConnectTimeout, string langTag = "en") { var uri = new UriBuilder(_baseUri) { Path = "/ws", Query = $"lang={langTag}&status={appearOnline}&token={session.AuthToken}" }.Uri; return _adapter.ConnectAsync(uri, connectTimeoutSec); } /// public Task ClosePartyAsync(string partyId) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyClose = new PartyClose { PartyId = partyId } }; return SendAsync(envelope); } /// public async Task CreateMatchAsync(string name = null) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", MatchCreate = new MatchCreateMessage { Name = name } }; var response = await SendAsync(envelope); return response.Match; } /// public async Task CreatePartyAsync(bool open, bool hidden, int maxSize, string label = null) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyCreate = new PartyCreate { Open = open, Hidden = hidden, MaxSize = maxSize, Label = label } }; var response = await SendAsync(envelope); return response.Party; } /// public Task FollowUsersAsync(IEnumerable users) => FollowUsersAsync(users.Select(user => user.Id)); /// public async Task FollowUsersAsync(IEnumerable userIDs, IEnumerable usernames = null) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", StatusFollow = new StatusFollowMessage { UserIds = new List(userIDs), Usernames = usernames != null ? new List(usernames) : new List() } }; var response = await SendAsync(envelope); return response.Status; } /// public async Task JoinChatAsync(string target, ChannelType type, bool persistence = false, bool hidden = false) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", ChannelJoin = new ChannelJoinMessage { Hidden = hidden, Persistence = persistence, Target = target, Type = (int)type } }; var response = await SendAsync(envelope); return response.Channel; } /// public async Task JoinMatchAsync(IMatchmakerMatched matched) { var message = new MatchJoinMessage(); if (matched.Token != null) { message.Token = matched.Token; } else { message.MatchId = matched.MatchId; } int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", MatchJoin = message }; var response = await SendAsync(envelope); return response.Match; } /// public async Task JoinMatchAsync(string matchId, IDictionary metadata = null) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", MatchJoin = new MatchJoinMessage { MatchId = matchId, Metadata = metadata } }; var response = await SendAsync(envelope); return response.Match; } /// public Task JoinPartyAsync(string partyId) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyJoin = new PartyJoin { PartyId = partyId } }; return SendAsync(envelope); } /// public Task LeaveChatAsync(IChannel channel) => LeaveChatAsync(channel.Id); /// public Task LeaveChatAsync(string channelId) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", ChannelLeave = new ChannelLeaveMessage { ChannelId = channelId } }; return SendAsync(envelope); } /// public Task LeaveMatchAsync(IMatch match) => LeaveMatchAsync(match.Id); /// public Task LeaveMatchAsync(string matchId) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", MatchLeave = new MatchLeaveMessage { MatchId = matchId } }; return SendAsync(envelope); } /// public Task LeavePartyAsync(string partyId) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyLeave = new PartyLeave { PartyId = partyId } }; return SendAsync(envelope); } /// public async Task ListPartyJoinRequestsAsync(string partyId) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyJoinRequestList = new PartyJoinRequestList { PartyId = partyId, } }; var response = await SendAsync(envelope); return response.PartyJoinRequest; } /// public Task PromotePartyMemberAsync(string partyId, IUserPresence partyMember) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyPromote = new PartyPromote { PartyId = partyId, Presence = partyMember as UserPresence // TODO serialize interface directly in protobuf } }; return SendAsync(envelope); } /// public Task RemoveChatMessageAsync(IChannel channel, string messageId) => RemoveChatMessageAsync(channel.Id, messageId); /// public async Task RemoveChatMessageAsync(string channelId, string messageId) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", ChannelMessageRemove = new ChannelRemoveMessage { ChannelId = channelId, MessageId = messageId } }; var response = await SendAsync(envelope); return response.ChannelMessageAck; } /// public Task RemoveMatchmakerAsync(IMatchmakerTicket ticket) => RemoveMatchmakerAsync(ticket.Ticket); /// public Task RemoveMatchmakerAsync(string ticket) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", MatchmakerRemove = new MatchmakerRemoveMessage { Ticket = ticket } }; return SendAsync(envelope); } /// public Task RemoveMatchmakerPartyAsync(string partyId, string ticket) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyMatchmakerRemove = new PartyMatchmakerRemove { PartyId = partyId, Ticket = ticket } }; return SendAsync(envelope); } /// public Task RemovePartyMemberAsync(string partyId, IUserPresence presence) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyMemberRemove = new PartyMemberRemove { PartyId = partyId, Presence = presence as UserPresence } }; return SendAsync(envelope); } /// public async Task RpcAsync(string funcId, string payload = null) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", Rpc = new ApiRpc { Id = funcId, Payload = payload } }; var response = await SendAsync(envelope); return response.Rpc; } /// public async Task RpcAsync(string funcId, ArraySegment payload) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", Rpc = new ApiRpc { Id = funcId, Payload = Convert.ToBase64String(payload.Array, payload.Offset, payload.Count) } }; var response = await SendAsync(envelope); return response.Rpc; } /// public Task SendMatchStateAsync(string matchId, long opCode, ArraySegment state, IEnumerable presences = null) { var envelope = new WebSocketMessageEnvelope { MatchStateSend = new MatchSendMessage { MatchId = matchId, OpCode = Convert.ToString(opCode), Presences = BuildPresenceList(presences), State = Convert.ToBase64String(state.Array, state.Offset, state.Count) } }; return SendAsync(envelope); } /// public Task SendMatchStateAsync(string matchId, long opCode, string state, IEnumerable presences = null) => SendMatchStateAsync(matchId, opCode, System.Text.Encoding.UTF8.GetBytes(state), presences); /// public Task SendMatchStateAsync(string matchId, long opCode, byte[] state, IEnumerable presences = null) { var envelope = new WebSocketMessageEnvelope { MatchStateSend = new MatchSendMessage { MatchId = matchId, OpCode = Convert.ToString(opCode), Presences = BuildPresenceList(presences), State = Convert.ToBase64String(state) } }; return SendAsync(envelope); } /// public Task SendPartyDataAsync(string partyId, long opCode, ArraySegment data) { var envelope = new WebSocketMessageEnvelope { PartyDataSend = new PartyDataSend { PartyId = partyId, OpCode = Convert.ToString(opCode), Data = Convert.ToBase64String(data.Array, data.Offset, data.Count) } }; return SendAsync(envelope); } /// public Task SendPartyDataAsync(string partyId, long opCode, string data) => SendPartyDataAsync(partyId, opCode, System.Text.Encoding.UTF8.GetBytes(data)); /// public Task SendPartyDataAsync(string partyId, long opCode, byte[] data) { var envelope = new WebSocketMessageEnvelope { PartyDataSend = new PartyDataSend { PartyId = partyId, OpCode = Convert.ToString(opCode), Data = Convert.ToBase64String(data) } }; return SendAsync(envelope); } public override string ToString() { return $"Socket(_baseUri='{_baseUri}', _cid={_cid}, IsConnected={IsConnected}, IsConnecting={IsConnecting})"; } /// public Task UnfollowUsersAsync(IEnumerable users) => UnfollowUsersAsync(users.Select(user => user.Id)); /// public Task UnfollowUsersAsync(IEnumerable userIDs) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", StatusUnfollow = new StatusUnfollowMessage { UserIds = new List(userIDs) } }; return SendAsync(envelope); } /// public Task UpdateChatMessageAsync(IChannel channel, string messageId, string content) => UpdateChatMessageAsync(channel.Id, messageId, content); /// public async Task UpdateChatMessageAsync(string channelId, string messageId, string content) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", ChannelMessageUpdate = new ChannelUpdateMessage { ChannelId = channelId, MessageId = messageId, Content = content } }; var response = await SendAsync(envelope); return response.ChannelMessageAck; } public async Task UpdatePartyAsync(string partyId, bool open, bool hidden, string label) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", PartyUpdate = new PartyUpdate { PartyId = partyId, Label = label, Open = open, Hidden = hidden } }; var response = await SendAsync(envelope); return response.PartyUpdate; } /// public Task UpdateStatusAsync(string status) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", StatusUpdate = new StatusUpdateMessage { Status = status } }; return SendAsync(envelope); } /// public Task WriteChatMessageAsync(IChannel channel, string content) => WriteChatMessageAsync(channel.Id, content); /// public async Task WriteChatMessageAsync(string channelId, string content) { int cid = Interlocked.Increment(ref _cid); var envelope = new WebSocketMessageEnvelope { Cid = $"{cid}", ChannelMessageSend = new ChannelSendMessage { ChannelId = channelId, Content = content } }; var response = await SendAsync(envelope); return response.ChannelMessageAck; } /// /// Build a socket from a client object. /// /// A client object. /// A new socket with the connection settings from the client. public static ISocket From(IClient client) => From(client, new WebSocketAdapter()); /// /// Build a socket from a client object and socket adapter. /// /// A client object. /// The socket adapter to use with the connection. /// A new socket with connection settings from the client. public static ISocket From(IClient client, ISocketAdapter adapter) { var scheme = client.Scheme.ToLower().Equals("http") ? "ws" : "wss"; return new Socket(scheme, client.Host, client.Port, adapter) { Logger = client.Logger }; } private void ProcessMessage(ArraySegment buffer) { var contents = System.Text.Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); Logger?.DebugFormat("Received JSON over web socket: {0}", contents); var envelope = contents.FromJson(); try { if (!string.IsNullOrEmpty(envelope.Cid)) { lock (_responsesLock) { // Handle message response. if (_responses.ContainsKey(envelope.Cid)) { var completer = _responses[envelope.Cid]; _responses.Remove(envelope.Cid); if (envelope.Error != null) { completer.SetException(new WebSocketException(WebSocketError.InvalidState, envelope.Error.Message)); } else { completer.SetResult(envelope); } } else { // it is valid for this to occur if a completer timed out and was // removed from the responses dictionary after the timeout. Logger?.WarnFormat("No completer for message cid: {0}", envelope.Cid); } } } else if (envelope.Error != null) { ReceivedError?.Invoke(new WebSocketException(WebSocketError.InvalidState, envelope.Error.Message)); } else if (envelope.ChannelMessage != null) { ReceivedChannelMessage?.Invoke(envelope.ChannelMessage); } else if (envelope.ChannelPresenceEvent != null) { ReceivedChannelPresence?.Invoke(envelope.ChannelPresenceEvent); } else if (envelope.MatchmakerMatched != null) { ReceivedMatchmakerMatched?.Invoke(envelope.MatchmakerMatched); } else if (envelope.MatchPresenceEvent != null) { ReceivedMatchPresence?.Invoke(envelope.MatchPresenceEvent); } else if (envelope.MatchState != null) { ReceivedMatchState?.Invoke(envelope.MatchState); } else if (envelope.NotificationList != null) { foreach (var notification in envelope.NotificationList.Notifications) { ReceivedNotification?.Invoke(notification); } } else if (envelope.StatusPresenceEvent != null) { ReceivedStatusPresence?.Invoke(envelope.StatusPresenceEvent); } else if (envelope.StreamPresenceEvent != null) { ReceivedStreamPresence?.Invoke(envelope.StreamPresenceEvent); } else if (envelope.StreamState != null) { ReceivedStreamState?.Invoke(envelope.StreamState); } else if (envelope.Party != null) { ReceivedParty?.Invoke(envelope.Party); } else if (envelope.PartyClose != null) { ReceivedPartyClose?.Invoke(envelope.PartyClose); } else if (envelope.PartyData != null) { ReceivedPartyData?.Invoke(envelope.PartyData); } else if (envelope.PartyUpdate != null) { ReceivedPartyUpdate?.Invoke(envelope.PartyUpdate); } else if (envelope.PartyJoinRequest != null) { ReceivedPartyJoinRequest?.Invoke(envelope.PartyJoinRequest); } else if (envelope.PartyLeader != null) { ReceivedPartyLeader?.Invoke(envelope.PartyLeader); } else if (envelope.PartyMatchmakerTicket != null) { ReceivedPartyMatchmakerTicket?.Invoke(envelope.PartyMatchmakerTicket); } else if (envelope.PartyPresenceEvent != null) { ReceivedPartyPresence?.Invoke(envelope.PartyPresenceEvent); } else { Logger?.ErrorFormat("Received unrecognised message: '{0}'", contents); } } catch (Exception e) { ReceivedError?.Invoke(e); } } private async Task SendAsync(WebSocketMessageEnvelope envelope) { var json = envelope.ToJson(); Logger?.DebugFormat("Sending JSON over web socket: {0}", json); var buffer = System.Text.Encoding.UTF8.GetBytes(json); var cts = new CancellationTokenSource(_sendTimeoutSec); if (string.IsNullOrEmpty(envelope.Cid)) { await _adapter.SendAsync(new ArraySegment(buffer), true, cts.Token); return null; // No response required. } var completer = new TaskCompletionSource(); lock (_responsesLock) { _responses[envelope.Cid] = completer; } cts.Token.Register(() => { lock (_responsesLock) { if (_responses.ContainsKey(envelope.Cid)) { _responses.Remove(envelope.Cid); } } completer.TrySetCanceled(); }); await _adapter.SendAsync(new ArraySegment(buffer), true, cts.Token); return await completer.Task; } private static List BuildPresenceList(IEnumerable presences) { if (presences == null) { return (List)UserPresence.NoPresences; } var presenceList = presences as List; if (presenceList != null) { return presenceList; } presenceList = new List(); foreach (var userPresence in presences) { var concretePresence = (UserPresence)userPresence; presenceList.Add(concretePresence); } return presenceList; } } } ================================================ FILE: Nakama/StatusFollowMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// Follow one or more other users for status updates. /// internal class StatusFollowMessage { [DataMember(Name = "user_ids"), Preserve] public List UserIds { get; set; } [DataMember(Name = "usernames"), Preserve] public List Usernames { get; set; } public override string ToString() { var userIds = string.Join(", ", UserIds); var usernames = string.Join(", ", Usernames); return $"StatusFollowMessage(UserIds=[{userIds}],Usernames=[{usernames}])"; } } } ================================================ FILE: Nakama/StatusUnfollowMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// Unfollow one or more users on the server. /// internal class StatusUnfollowMessage { [DataMember(Name="user_ids"), Preserve] public List UserIds { get; set; } public override string ToString() { var userIds = string.Join(", ", UserIds); return $"StatusUnfollowMessage(UserIds=[{userIds}])"; } } } ================================================ FILE: Nakama/StatusUpdateMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// Update the status of the current user. /// internal class StatusUpdateMessage { [DataMember(Name="status"), Preserve] public string Status { get; set; } public override string ToString() { return $"StatusUpdateMessage(Status='{Status}')"; } } } ================================================ FILE: Nakama/StorageObjectId.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama { /// /// An identifier for a storage object. /// /// public class StorageObjectId : IApiReadStorageObjectId, IApiDeleteStorageObjectId { /// public string Collection { get; set; } /// public string Key { get; set; } /// public string Version { get; set; } /// public string UserId { get; set; } public override string ToString() { return $"StorageObjectId(Collection='{Collection}', Key='{Key}', Version='{Version}', UserId='{UserId}')"; } } } ================================================ FILE: Nakama/TinyJson/JsonParser.cs ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Alex Parker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; namespace Nakama.TinyJson { // Really simple JSON parser in ~300 lines // - Attempts to parse JSON files with minimal GC allocation // - Nice and simple "[1,2,3]".FromJson>() API // - Classes and structs can be parsed too! // class Foo { public int Value; } // "{\"Value\":10}".FromJson() // - Can parse JSON without type information into Dictionary and List e.g. // "[1,2,3]".FromJson().GetType() == typeof(List) // "{\"Value\":10}".FromJson().GetType() == typeof(Dictionary) // - No JIT Emit support to support AOT compilation on iOS // - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead. // - Only public fields and property setters on classes/structs will be written to // // Limitations: // - No JIT Emit support to parse structures quickly // - Limited to parsing <2GB JSON files (due to int.MaxValue) // - Parsing of abstract classes or interfaces is NOT supported and will throw an exception. public static class JsonParser { [ThreadStatic] private static Stack> _splitArrayPool; [ThreadStatic] private static StringBuilder _stringBuilder; [ThreadStatic] private static Dictionary> _fieldInfoCache; [ThreadStatic] private static Dictionary> _propertyInfoCache; public static T FromJson(this string json) { // Initialize, if needed, the ThreadStatic variables if (_propertyInfoCache == null) { _propertyInfoCache = new Dictionary>(); } if (_fieldInfoCache == null) { _fieldInfoCache = new Dictionary>(); } if (_stringBuilder == null) { _stringBuilder = new StringBuilder(); } if (_splitArrayPool == null) { _splitArrayPool = new Stack>(); } // Remove all whitespace not within strings to make parsing simpler _stringBuilder.Length = 0; for (var i = 0; i < json.Length; i++) { var c = json[i]; if (c == '"') { i = AppendUntilStringEnd(true, i, json); continue; } if (char.IsWhiteSpace(c)) continue; _stringBuilder.Append(c); } //Parse the thing! return (T) ParseValue(typeof(T), _stringBuilder.ToString()); } private static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) { _stringBuilder.Append(json[startIdx]); for (var i = startIdx + 1; i < json.Length; i++) { if (json[i] == '\\') { if (appendEscapeCharacter) _stringBuilder.Append(json[i]); _stringBuilder.Append(json[i + 1]); i++; //Skip next character as it is escaped } else if (json[i] == '"') { _stringBuilder.Append(json[i]); return i; } else { _stringBuilder.Append(json[i]); } } return json.Length - 1; } //Splits { :, : } and [ , ] into a list of strings private static List Split(string json) { var splitArray = _splitArrayPool.Count > 0 ? _splitArrayPool.Pop() : new List(); splitArray.Clear(); if (json.Length == 2) return splitArray; var parseDepth = 0; _stringBuilder.Length = 0; for (var i = 1; i < json.Length - 1; i++) { if (json[i] == '[' || json[i] == '{') { parseDepth++; } else if (json[i] == ']' || json[i] == '}') { parseDepth--; } else if (json[i] == '"') { i = AppendUntilStringEnd(true, i, json); continue; } else if (json[i] == ',' || json[i] == ':') { if (parseDepth == 0) { splitArray.Add(_stringBuilder.ToString()); _stringBuilder.Length = 0; continue; } } _stringBuilder.Append(json[i]); } splitArray.Add(_stringBuilder.ToString()); return splitArray; } private static object ParseValue(Type type, string json) { if (type == typeof(string)) { // Return the raw value if it is unquoted (e.g. a number) var validQuotes = new[] {'"', '\''}; if (json.Length > 0 && !validQuotes.Contains(json[0]) && !validQuotes.Contains(json[json.Length-1])) { return json; } if (json.Length <= 2) return string.Empty; var parseStringBuilder = new StringBuilder(json.Length); for (var i = 1; i < json.Length - 1; ++i) { if (json[i] == '\\' && i + 1 < json.Length - 1) { var j = "\"\\nrtbf/".IndexOf(json[i + 1]); if (j >= 0) { parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]); ++i; continue; } if (json[i + 1] == 'u' && i + 5 < json.Length - 1) { uint c; if (uint.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c)) { parseStringBuilder.Append((char) c); i += 5; continue; } } } parseStringBuilder.Append(json[i]); } return parseStringBuilder.ToString(); } if (type.IsPrimitive) { var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture); return result; } if (type == typeof(decimal)) { decimal result; decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); return result; } if (json == "null") { return null; } if (type.IsEnum) { if (json[0] == '"') json = json.Substring(1, json.Length - 2); try { return Enum.Parse(type, json, false); } catch { return 0; } } if (type.IsArray) { var arrayType = type.GetElementType(); if (json[0] != '[' || json[json.Length - 1] != ']') return null; var elems = Split(json); var newArray = Array.CreateInstance(arrayType, elems.Count); for (var i = 0; i < elems.Count; i++) newArray.SetValue(ParseValue(arrayType, elems[i]), i); _splitArrayPool.Push(elems); return newArray; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { var listType = type.GetGenericArguments()[0]; if (json[0] != '[' || json[json.Length - 1] != ']') return null; var elems = Split(json); var list = (IList) type.GetConstructor(new Type[] {typeof(int)}).Invoke(new object[] {elems.Count}); foreach (var t in elems) list.Add(ParseValue(listType, t)); _splitArrayPool.Push(elems); return list; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { Type keyType, valueType; { var args = type.GetGenericArguments(); keyType = args[0]; valueType = args[1]; } // Refuse to parse dictionary keys that aren't of type string if (keyType != typeof(string)) return null; // Must be a valid dictionary element if (json[0] != '{' || json[json.Length - 1] != '}') return null; // The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON var elems = Split(json); if (elems.Count % 2 != 0) return null; var dictionary = (IDictionary) type.GetConstructor(new Type[] {typeof(int)}) .Invoke(new object[] {elems.Count / 2}); for (var i = 0; i < elems.Count; i += 2) { if (elems[i].Length <= 2) continue; var keyValue = elems[i].Substring(1, elems[i].Length - 2); var val = ParseValue(valueType, elems[i + 1]); dictionary.Add(keyValue, val); } return dictionary; } if (type == typeof(object)) { return ParseAnonymousValue(json); } if (json[0] == '{' && json[json.Length - 1] == '}') { return ParseObject(type, json); } return null; } private static object ParseAnonymousValue(string json) { if (json.Length == 0) return null; if (json[0] == '{' && json[json.Length - 1] == '}') { var elems = Split(json); if (elems.Count % 2 != 0) return null; var dict = new Dictionary(elems.Count / 2); for (var i = 0; i < elems.Count; i += 2) dict.Add(elems[i].Substring(1, elems[i].Length - 2), ParseAnonymousValue(elems[i + 1])); return dict; } if (json[0] == '[' && json[json.Length - 1] == ']') { var items = Split(json); var finalList = new List(items.Count); foreach (var t in items) finalList.Add(ParseAnonymousValue(t)); return finalList; } if (json[0] == '"' && json[json.Length - 1] == '"') { var str = json.Substring(1, json.Length - 2); return str.Replace("\\", string.Empty); } if (char.IsDigit(json[0]) || json[0] == '-') { if (json.Contains(".")) { double result; double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); return result; } else { int result; int.TryParse(json, out result); return result; } } if (json == "true") return true; if (json == "false") return false; // handles json == "null" as well as invalid JSON return null; } private static Dictionary CreateMemberNameDictionary(IEnumerable members) where T : MemberInfo { // NOTE The StringComparer is disabled intentionally because of how our generated code. // var nameToMember = new Dictionary(StringComparer.OrdinalIgnoreCase); var nameToMember = new Dictionary(); foreach (var member in members) { if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue; var name = member.Name; if (member.IsDefined(typeof(DataMemberAttribute), true)) { var dataMemberAttribute = (DataMemberAttribute) Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) name = dataMemberAttribute.Name; } nameToMember.Add(name, member); } return nameToMember; } private static object ParseObject(Type type, string json) { var instance = FormatterServices.GetUninitializedObject(type); // The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON var elems = Split(json); if (elems.Count % 2 != 0) return instance; Dictionary nameToField; Dictionary nameToProperty; if (!_fieldInfoCache.TryGetValue(type, out nameToField)) { nameToField = CreateMemberNameDictionary( type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); _fieldInfoCache.Add(type, nameToField); } if (!_propertyInfoCache.TryGetValue(type, out nameToProperty)) { nameToProperty = CreateMemberNameDictionary( type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); _propertyInfoCache.Add(type, nameToProperty); } for (var i = 0; i < elems.Count; i += 2) { if (elems[i].Length <= 2) continue; var key = elems[i].Substring(1, elems[i].Length - 2); var value = elems[i + 1]; FieldInfo fieldInfo; PropertyInfo propertyInfo; if (nameToField.TryGetValue(key, out fieldInfo)) fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value)); else if (nameToProperty.TryGetValue(key, out propertyInfo)) propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null); } return instance; } } } ================================================ FILE: Nakama/TinyJson/JsonWriter.cs ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Alex Parker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Runtime.Serialization; using System.Text; namespace Nakama.TinyJson { // Really simple JSON writer // - Outputs JSON structures from an object // - Really simple API (new List { 1, 2, 3 }).ToJson() == "[1,2,3]" // - Will only output public fields and property getters on objects public static class JsonWriter { public static string ToJson(this object item) { var stringBuilder = new StringBuilder(); AppendValue(stringBuilder, item); return stringBuilder.ToString(); } private static void AppendValue(StringBuilder stringBuilder, object item) { if (item == null) { stringBuilder.Append("null"); return; } var type = item.GetType(); if (type == typeof(string) || type == typeof(char)) { stringBuilder.Append('"'); var str = (string) item; foreach (var t in str) if (t < ' ' || t == '"' || t == '\\') { stringBuilder.Append('\\'); var j = "\"\\\n\r\t\b\f".IndexOf(t); if (j >= 0) stringBuilder.Append("\"\\nrtbf"[j]); else stringBuilder.AppendFormat("u{0:X4}", (uint) t); } else stringBuilder.Append(t); stringBuilder.Append('"'); } else if (type == typeof(byte) || type == typeof(sbyte)) { stringBuilder.Append(item); } else if (type == typeof(short) || type == typeof(ushort)) { stringBuilder.Append(item); } else if (type == typeof(int) || type == typeof(uint)) { stringBuilder.Append(item); } else if (type == typeof(long) || type == typeof(ulong)) { stringBuilder.Append(item); } else if (type == typeof(float)) { stringBuilder.Append(((float) item).ToString(System.Globalization.CultureInfo.InvariantCulture)); } else if (type == typeof(double)) { stringBuilder.Append(((double) item).ToString(System.Globalization.CultureInfo.InvariantCulture)); } else if (type == typeof (decimal)) { stringBuilder.Append (((decimal) item).ToString (System.Globalization.CultureInfo.InvariantCulture)); } else if (type == typeof(bool)) { stringBuilder.Append((bool) item ? "true" : "false"); } else if (type.IsEnum) { stringBuilder.Append('"'); stringBuilder.Append(item); stringBuilder.Append('"'); } else if (item is IList) { stringBuilder.Append('['); var isFirst = true; var list = (IList) item; foreach (var t in list) { if (isFirst) isFirst = false; else stringBuilder.Append(','); AppendValue(stringBuilder, t); } stringBuilder.Append(']'); } else if (item is IDictionary dict) { var keyType = type.GetGenericArguments()[0]; //Refuse to output dictionary keys that aren't of type string if (keyType != typeof(string)) { stringBuilder.Append("{}"); return; } stringBuilder.Append('{'); var isFirst = true; foreach (var key in dict.Keys) { if (isFirst) isFirst = false; else stringBuilder.Append(','); stringBuilder.Append('\"'); stringBuilder.Append((string) key); stringBuilder.Append("\":"); AppendValue(stringBuilder, dict[key]); } stringBuilder.Append('}'); } else { stringBuilder.Append('{'); var isFirst = true; var fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); foreach (var t in fieldInfos) { if (t.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue; var value = t.GetValue(item); if (value == null) continue; if (isFirst) isFirst = false; else stringBuilder.Append(','); stringBuilder.Append('\"'); stringBuilder.Append(GetMemberName(t)); stringBuilder.Append("\":"); AppendValue(stringBuilder, value); } var propertyInfo = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); foreach (var t in propertyInfo) { if (!t.CanRead || t.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue; var value = t.GetValue(item, null); if (value == null) continue; if (isFirst) isFirst = false; else stringBuilder.Append(','); stringBuilder.Append('\"'); stringBuilder.Append(GetMemberName(t)); stringBuilder.Append("\":"); AppendValue(stringBuilder, value); } stringBuilder.Append('}'); } } private static string GetMemberName(MemberInfo member) { if (!member.IsDefined(typeof(DataMemberAttribute), true)) return member.Name; var dataMemberAttribute = (DataMemberAttribute) Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); return !string.IsNullOrEmpty(dataMemberAttribute.Name) ? dataMemberAttribute.Name : member.Name; } } } ================================================ FILE: Nakama/TinyJson/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018 Alex Parker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Nakama/TransientExceptionDelegate.cs ================================================ using System; namespace Nakama { /// /// A delegate used to determine whether or not a network exception is /// due to a temporary bad state on the server. For example, timeouts can be transient in cases where /// the server is experiencing temporarily high load. /// public delegate bool TransientExceptionDelegate(Exception e); } ================================================ FILE: Nakama/WebSocketAdapter.cs ================================================ // Copyright 2019 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.IO; using System.Net.Sockets; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Nakama.Ninja.WebSockets; namespace Nakama { /// /// An adapter which uses the WebSocket protocol with Nakama server. /// public class WebSocketAdapter : ISocketAdapter { private const int KeepAliveIntervalSec = 15; private const int MaxMessageReadSize = 1024 * 256; private const int SendTimeoutSec = 10; /// public event Action Connected; /// public event Action Closed; /// public event Action ReceivedError; /// public event Action> Received; /// /// If the WebSocket is connected. /// public bool IsConnected => _webSocket?.State == WebSocketState.Open; /// /// If the WebSocket is connecting. /// public bool IsConnecting => _webSocket?.State == WebSocketState.Connecting; private readonly int _maxMessageReadSize; private readonly WebSocketClientOptions _options; private readonly TimeSpan _sendTimeoutSec; private CancellationTokenSource _cancellationSource; private WebSocket _webSocket; private Uri _uri; private readonly ILogger _logger; public WebSocketAdapter(int keepAliveIntervalSec = KeepAliveIntervalSec, int sendTimeoutSec = SendTimeoutSec, int maxMessageReadSize = MaxMessageReadSize, ILogger logger = null) : this(new WebSocketClientOptions { IncludeExceptionInCloseResponse = true, KeepAliveInterval = TimeSpan.FromSeconds(keepAliveIntervalSec), NoDelay = true }, sendTimeoutSec, maxMessageReadSize, logger) { } public WebSocketAdapter(WebSocketClientOptions options, int sendTimeoutSec, int maxMessageReadSize, ILogger logger) { _maxMessageReadSize = maxMessageReadSize; _options = options; _sendTimeoutSec = TimeSpan.FromSeconds(sendTimeoutSec); _logger = logger; } /// public async Task CloseAsync() { if (_webSocket == null) return; if (_webSocket.State == WebSocketState.Open) { await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); } else if (_webSocket.State == WebSocketState.Connecting) { // cancel mid-connect _cancellationSource?.Cancel(); } _webSocket = null; } /// public async Task ConnectAsync(Uri uri, int timeout) { if (_webSocket?.State == WebSocketState.Open || _webSocket?.State == WebSocketState.Connecting) { // Already connected so we can return. return; } _cancellationSource = new CancellationTokenSource(); _uri = uri; var clientFactory = new WebSocketClientFactory(); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationSource.Token, cts.Token); _webSocket = await clientFactory.ConnectAsync(_uri, _options, linkedCts.Token).ConfigureAwait(false); _ = Task.Factory.StartNew(_ => ReceiveLoop(_webSocket, _cancellationSource.Token), TaskCreationOptions.LongRunning, _cancellationSource.Token); Connected?.Invoke(); } /// public Task SendAsync(ArraySegment buffer, bool reliable = true, CancellationToken canceller = default) { if (_webSocket?.State != WebSocketState.Open) { throw new SocketException((int)SocketError.NotConnected); } canceller.ThrowIfCancellationRequested(); try { var cts = new CancellationTokenSource(_sendTimeoutSec); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(canceller, cts.Token); var t = _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, linkedCts.Token); t.ConfigureAwait(false); return t; } catch { _ = CloseAsync(); throw; } } /// public override string ToString() => $"WebSocketAdapter(MaxMessageSize={_maxMessageReadSize}, Uri='{_uri}')"; private async Task ReceiveLoop(WebSocket webSocket, CancellationToken canceller) { canceller.ThrowIfCancellationRequested(); var buffer = new byte[_maxMessageReadSize]; var bufferReadCount = 0; var closeReason = ""; try { do { var bufferSegment = new ArraySegment(buffer, bufferReadCount, _maxMessageReadSize - bufferReadCount); var result = await webSocket.ReceiveAsync(bufferSegment, canceller).ConfigureAwait(false); if (result == null) { break; } if (result.MessageType == WebSocketMessageType.Close) { if (webSocket.State == WebSocketState.CloseReceived) { try { closeReason = result.CloseStatusDescription ?? ""; await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); } // Ignore these exceptions from CloseOutputAsync as we're already closing the socket // anyway. In the MonoRuntime, the Close message can be received after the socket has // been disposed, causing these exceptions to be thrown. catch (WebSocketException e) when (e.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { // ignored. } catch (Exception e) when (e is ObjectDisposedException || e is InvalidOperationException || e is IOException) { // ignored. } } break; } bufferReadCount += result.Count; if (!result.EndOfMessage) continue; try { Received?.Invoke(new ArraySegment(buffer, 0, bufferReadCount)); } catch (Exception e) { // Don't stop receive loop if received function throws. ReceivedError?.Invoke(e); } bufferReadCount = 0; } while (!canceller.IsCancellationRequested && _webSocket?.State == WebSocketState.Open); } catch (EndOfStreamException) { // IGNORE: // "Unexpected end of stream encountered whilst attempting to read 2 bytes." } catch (IOException) { // IGNORE. } catch (SocketException) { // IGNORE: // "Unable to read data from the transport connection: Access denied." // "Unable to read data from the transport connection: Network subsystem is down." // "Unable to write data to the transport connection: The socket has been shut down." // "The socket is not connected" // "Unable to read data from the transport connection: Connection reset by peer." // "Unable to read data from the transport connection: Connection timed out." // "Unable to read data from the transport connection: Connection refused." } catch (Exception e) { ReceivedError?.Invoke(e); } finally { Closed?.Invoke(closeReason); } } } } ================================================ FILE: Nakama/WebSocketErrorMessage.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Nakama { /// /// A logical error received on the WebSocket connection. /// internal class WebSocketErrorMessage { [DataMember(Name = "code"), Preserve] public int Code { get; set; } [DataMember(Name = "context"), Preserve] public Dictionary Context { get; set; } [DataMember(Name = "message"), Preserve] public string Message { get; set; } public override string ToString() { return $"WebSocketErrorMessage(Code={Code}, Context={Context}, Message='{Message}')"; } } } ================================================ FILE: Nakama/WebSocketMessageEnvelope.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.Serialization; namespace Nakama { /// /// An envelope for messages received or sent on a WebSocket. /// internal class WebSocketMessageEnvelope { [DataMember(Name="cid"), Preserve] public string Cid { get; set; } [DataMember(Name="channel"), Preserve] public Channel Channel { get; set; } [DataMember(Name="channel_join"), Preserve] public ChannelJoinMessage ChannelJoin { get; set; } [DataMember(Name="channel_leave"), Preserve] public ChannelLeaveMessage ChannelLeave { get; set; } [DataMember(Name="channel_message"), Preserve] public ApiChannelMessage ChannelMessage { get; set; } [DataMember(Name="channel_message_ack"), Preserve] public ChannelMessageAck ChannelMessageAck { get; set; } [DataMember(Name="channel_message_remove"), Preserve] public ChannelRemoveMessage ChannelMessageRemove { get; set; } [DataMember(Name="channel_message_send"), Preserve] public ChannelSendMessage ChannelMessageSend { get; set; } [DataMember(Name="channel_message_update"), Preserve] public ChannelUpdateMessage ChannelMessageUpdate { get; set; } [DataMember(Name="channel_presence_event"), Preserve] public ChannelPresenceEvent ChannelPresenceEvent { get; set; } [DataMember(Name="error"), Preserve] public WebSocketErrorMessage Error { get; set; } [DataMember(Name="matchmaker_add"), Preserve] public MatchmakerAddMessage MatchmakerAdd { get; set; } [DataMember(Name="matchmaker_matched"), Preserve] public MatchmakerMatched MatchmakerMatched { get; set; } [DataMember(Name="matchmaker_remove"), Preserve] public MatchmakerRemoveMessage MatchmakerRemove { get; set; } [DataMember(Name="matchmaker_ticket"), Preserve] public MatchmakerTicket MatchmakerTicket { get; set; } [DataMember(Name="match"), Preserve] public Match Match { get; set; } [DataMember(Name="match_create"), Preserve] public MatchCreateMessage MatchCreate { get; set; } [DataMember(Name="match_join"), Preserve] public MatchJoinMessage MatchJoin { get; set; } [DataMember(Name="match_leave"), Preserve] public MatchLeaveMessage MatchLeave { get; set; } [DataMember(Name="match_presence_event"), Preserve] public MatchPresenceEvent MatchPresenceEvent { get; set; } [DataMember(Name="match_data"), Preserve] public MatchState MatchState { get; set; } [DataMember(Name="match_data_send"), Preserve] public MatchSendMessage MatchStateSend { get; set; } [DataMember(Name="notifications"), Preserve] public ApiNotificationList NotificationList { get; set; } [DataMember(Name="rpc"), Preserve] public ApiRpc Rpc { get; set; } [DataMember(Name="status"), Preserve] public Status Status { get; set; } [DataMember(Name="status_follow"), Preserve] public StatusFollowMessage StatusFollow { get; set; } [DataMember(Name="status_presence_event"), Preserve] public StatusPresenceEvent StatusPresenceEvent { get; set; } [DataMember(Name="status_unfollow"), Preserve] public StatusUnfollowMessage StatusUnfollow { get; set; } [DataMember(Name="status_update"), Preserve] public StatusUpdateMessage StatusUpdate { get; set; } [DataMember(Name="stream_presence_event"), Preserve] public StreamPresenceEvent StreamPresenceEvent { get; set; } [DataMember(Name="stream_data"), Preserve] public StreamState StreamState { get; set; } [DataMember(Name="party"), Preserve] public Party Party { get; set; } [DataMember(Name="party_create"), Preserve] public PartyCreate PartyCreate { get; set; } [DataMember(Name="party_update"), Preserve] public PartyUpdate PartyUpdate { get; set; } [DataMember(Name="party_join"), Preserve] public PartyJoin PartyJoin { get; set; } [DataMember(Name="party_leave"), Preserve] public PartyLeave PartyLeave { get; set; } [DataMember(Name="party_promote"), Preserve] public PartyPromote PartyPromote { get; set; } [DataMember(Name="party_leader"), Preserve] public PartyLeader PartyLeader { get; set; } [DataMember(Name="party_accept"), Preserve] public PartyAccept PartyAccept { get; set; } [DataMember(Name="party_remove"), Preserve] public PartyMemberRemove PartyMemberRemove { get; set; } [DataMember(Name="party_close"), Preserve] public PartyClose PartyClose { get; set; } [DataMember(Name="party_join_request_list"), Preserve] public PartyJoinRequestList PartyJoinRequestList { get; set; } [DataMember(Name="party_join_request"), Preserve] public PartyJoinRequest PartyJoinRequest { get; set; } [DataMember(Name="party_matchmaker_add"), Preserve] public PartyMatchmakerAdd PartyMatchmakerAdd { get; set; } [DataMember(Name="party_matchmaker_remove"), Preserve] public PartyMatchmakerRemove PartyMatchmakerRemove { get; set; } [DataMember(Name="party_matchmaker_ticket"), Preserve] public PartyMatchmakerTicket PartyMatchmakerTicket { get; set; } [DataMember(Name="party_data"), Preserve] public PartyData PartyData { get; set; } [DataMember(Name="party_data_send"), Preserve] public PartyDataSend PartyDataSend { get; set; } [DataMember(Name="party_presence_event"), Preserve] public PartyPresenceEvent PartyPresenceEvent { get; set; } public override string ToString() { return "WebSocketMessageEnvelope"; } } } ================================================ FILE: Nakama/WebSocketStdlibAdapter.cs ================================================ // Copyright 2022 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.IO; using System.Net.Sockets; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; namespace Nakama { /// /// An adapter which uses the WebSocket protocol with Nakama server. /// public class WebSocketStdlibAdapter : ISocketAdapter { private const int KeepAliveIntervalSec = 15; private const int MaxMessageReadSize = 1024 * 256; private const int SendTimeoutSec = 10; /// public event Action Connected; /// public event Action Closed; /// public event Action ReceivedError; /// public event Action> Received; /// /// If the WebSocket is connected. /// public bool IsConnected => _webSocket?.State == WebSocketState.Open; /// /// If the WebSocket is connecting. /// public bool IsConnecting => _webSocket?.State == WebSocketState.Connecting; private CancellationTokenSource _cancellationSource; private Uri _uri; private ClientWebSocket _webSocket; private readonly int _maxMessageReadSize; private readonly TimeSpan _sendTimeoutSec; public WebSocketStdlibAdapter(int sendTimeoutSec = SendTimeoutSec, int maxMessageReadSize = MaxMessageReadSize) { _maxMessageReadSize = maxMessageReadSize; _sendTimeoutSec = TimeSpan.FromSeconds(sendTimeoutSec); _webSocket = new ClientWebSocket(); } public WebSocketStdlibAdapter(ClientWebSocket webSocket) { // There is no way to override options so allow constructor to take a websocket that already has options. _webSocket = webSocket; } /// public async Task CloseAsync() { if (_webSocket == null) return; if (_webSocket.State == WebSocketState.Open) { await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); } else if (_webSocket.State == WebSocketState.Connecting) { // cancel mid-connect _cancellationSource?.Cancel(); } _webSocket = null; } /// public async Task ConnectAsync(Uri uri, int timeout) { if (_webSocket?.State == WebSocketState.Open || _webSocket?.State == WebSocketState.Connecting) { // Already connecting or connected so we can return. return; } _cancellationSource = new CancellationTokenSource(); _uri = uri; _webSocket = new ClientWebSocket(); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationSource.Token, cts.Token); await _webSocket.ConnectAsync(_uri, linkedCts.Token).ConfigureAwait(false); _ = ReceiveLoop(_webSocket, _cancellationSource.Token); Connected?.Invoke(); } /// public Task SendAsync(ArraySegment buffer, bool reliable = true, CancellationToken canceller = default) { if (_webSocket?.State != WebSocketState.Open) { throw new SocketException((int)SocketError.NotConnected); } canceller.ThrowIfCancellationRequested(); try { var cts = new CancellationTokenSource(_sendTimeoutSec); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(canceller, cts.Token); var t = _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, linkedCts.Token); t.ConfigureAwait(false); return t; } catch { _ = CloseAsync(); throw; } } /// public override string ToString() => $"WebSocketStdlibAdapter(MaxMessageSize={_maxMessageReadSize}, Uri='{_uri}')"; private async Task ReceiveLoop(WebSocket webSocket, CancellationToken canceller) { canceller.ThrowIfCancellationRequested(); var buffer = new byte[_maxMessageReadSize]; var bufferReadCount = 0; var closeReason = ""; try { do { var bufferSegment = new ArraySegment(buffer, bufferReadCount, _maxMessageReadSize - bufferReadCount); var result = await webSocket.ReceiveAsync(bufferSegment, canceller).ConfigureAwait(false); if (result == null) { break; } if (result.MessageType == WebSocketMessageType.Close) { if (webSocket.State == WebSocketState.CloseReceived) { try { closeReason = result.CloseStatusDescription ?? ""; await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); } // Ignore these exceptions from CloseOutputAsync as we're already closing the socket // anyway. In the MonoRuntime, the Close message can be received after the socket has // been disposed, causing these exceptions to be thrown. catch (WebSocketException e) when (e.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { // ignored. } catch (Exception e) when (e is ObjectDisposedException || e is InvalidOperationException || e is IOException) { // ignored. } } break; } bufferReadCount += result.Count; if (!result.EndOfMessage) continue; try { Received?.Invoke(new ArraySegment(buffer, 0, bufferReadCount)); } catch (Exception e) { ReceivedError?.Invoke(e); } bufferReadCount = 0; } while (!canceller.IsCancellationRequested && _webSocket?.State == WebSocketState.Open); } catch (Exception e) { ReceivedError?.Invoke(e); } finally { Closed?.Invoke(closeReason); } } } } ================================================ FILE: Nakama/WriteStorageObject.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama { /// public class WriteStorageObject : IApiWriteStorageObject { /// public string Collection { get; set; } /// public string Key { get; set; } /// public int PermissionRead { get; set; } = 1; /// public int PermissionWrite { get; set; } = 1; /// public string Value { get; set; } /// public string Version { get; set; } } } ================================================ FILE: Nakama.Tests/AssemblyInfo.cs ================================================ using Xunit; // resolve test hangs [assembly: CollectionBehavior(DisableTestParallelization = true)] ================================================ FILE: Nakama.Tests/AuthenticateTest.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Linq; using System.Net; using System.Threading.Tasks; using Xunit; namespace Nakama.Tests.Api { public class AuthenticateTest { private IClient _client; public AuthenticateTest() { _client = TestsUtil.FromSettingsFile(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldAuthenticateCustomId() { var customid = Guid.NewGuid(); var session = await _client.AuthenticateCustomAsync(customid.ToString()); Assert.NotNull(session); Assert.NotNull(session.UserId); Assert.NotNull(session.Username); Assert.False(session.IsExpired); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldAuthenticateDeviceId() { var deviceid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateDeviceAsync(deviceid); Assert.NotNull(session); Assert.NotNull(session.UserId); Assert.NotNull(session.Username); Assert.False(session.IsExpired); var account = await _client.GetAccountAsync(session); Assert.NotNull(account); Assert.Equal(1, account.Devices.Count(d => d.Id == deviceid)); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldAuthenticateDeviceAndSaveUsername() { var deviceid = Guid.NewGuid().ToString(); var username = Guid.NewGuid().ToString(); var session = await _client.AuthenticateDeviceAsync(deviceid, username); var account = await _client.GetAccountAsync(session); Assert.Equal(username, session.Username); Assert.Equal(username, account.User.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldAuthenticateEmail() { var session = await _client.AuthenticateEmailAsync("super@heroes.com", "batsignal"); Assert.NotNull(session); Assert.NotNull(session.UserId); Assert.NotNull(session.Username); Assert.False(session.IsExpired); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void ShouldNotAuthenticateFacebook() { var ex = await Assert.ThrowsAsync(() => _client.AuthenticateFacebookAsync("invalid")); Assert.Equal((int) HttpStatusCode.Unauthorized, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void ShouldNotAuthenticateGameCenter() { var bundleId = string.Empty; var playerId = string.Empty; var publicKeyUrl = string.Empty; var salt = string.Empty; var signature = string.Empty; var timestamp = string.Empty; var ex = await Assert.ThrowsAsync(() => _client.AuthenticateGameCenterAsync(bundleId, playerId, publicKeyUrl, salt, signature, timestamp)); Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void ShouldNotAuthenticateGoogle() { var ex = await Assert.ThrowsAsync(() => _client.AuthenticateGoogleAsync("invalid")); Assert.Equal((int) HttpStatusCode.Unauthorized, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void ShouldNotAuthenticateSteam() { var ex = await Assert.ThrowsAsync(() => _client.AuthenticateSteamAsync("invalid")); // Precondition failed because Steam requires special configuration with the server. // Maps to 400, because gRPC precondition failed != HTTP precondition failed. Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void ShouldNotAuthenticateApple() { // Fails because Apple requires special configuration with the server. var ex = await Assert.ThrowsAsync(() => _client.AuthenticateAppleAsync("some_token")); Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } } } ================================================ FILE: Nakama.Tests/AwaitedSocketTaskTest.cs ================================================ // Copyright 2019 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Net.Sockets; using System.Threading.Tasks; using Xunit; namespace Nakama.Tests { public class AwaitedSocketTaskTest : IDisposable { private IClient _client; private readonly ISocket _socket; public AwaitedSocketTaskTest() { _client = TestsUtil.FromSettingsFile(); _socket = Nakama.Socket.From(_client); } public void Dispose() => _client = null; [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void Socket_AwaitedTasks_AreCanceled() { var id = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(id); await _socket.ConnectAsync(session); var matchmakerTask1 = _socket.AddMatchmakerAsync("+label.foo:\"val\"", 15, 20); var matchmakerTask2 = _socket.AddMatchmakerAsync("+label.bar:\"val\"", 15, 20); await _socket.CloseAsync(); await Assert.ThrowsAsync(() => Task.WhenAll(matchmakerTask1, matchmakerTask2)); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void Socket_AwaitedTasksAfterDisconnect_ThrowException() { var id = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(id); await _socket.ConnectAsync(session); await _socket.CloseAsync(); var statusTask1 = _socket.FollowUsersAsync(new[] {session.UserId}); var statusTask2 = _socket.FollowUsersAsync(new[] {session.UserId}); await Assert.ThrowsAsync(() => Task.WhenAll(statusTask1, statusTask2)); } } } ================================================ FILE: Nakama.Tests/CancelTest.cs ================================================ /** * Copyright 2021 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Threading; using System.Threading.Tasks; using Xunit; namespace Nakama.Tests { public class CancelTest { [Fact] public async void TestBasicCancel() { var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath); var canceller = new CancellationTokenSource(); Task authTask = client.AuthenticateCustomAsync("test_id", null, true, null, null, canceller.Token); canceller.Cancel(); await Assert.ThrowsAsync(async () => await authTask); } [Fact] public async void TestCancelDuringBackoff() { var adapterSchedule = new TransientAdapterResponseType[3] { TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); var canceller = new CancellationTokenSource(); RetryListener retryListener = (int numRetry, Retry retry) => { canceller.Cancel(); }; Task authTask = client.AuthenticateCustomAsync("test_id", null, true, null, new RetryConfiguration(100, 2, retryListener), canceller.Token); await Assert.ThrowsAsync(async () => await authTask); } } } ================================================ FILE: Nakama.Tests/FriendTest.cs ================================================ /** * Copyright 2023 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Linq; using System.Threading.Tasks; using Xunit; namespace Nakama.Tests.Api { public class FriendTest { private IClient _client; private ISocket _socket; public FriendTest() { _client = TestsUtil.FromSettingsFile(); _socket = Nakama.Socket.From(_client); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task AddingBannedFriendShouldNoop() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session2); IApiNotification? session2Notif = null; _socket.ReceivedNotification += (IApiNotification notif) => { session2Notif = notif; }; await _client.BlockFriendsAsync(session, new string[]{session2.UserId}); await _client.AddFriendsAsync(session, new string[]{session2.UserId}); var friendList = await _client.ListFriendsAsync(session); Assert.Single(friendList.Friends); Assert.Equal(3, friendList.Friends.First().State); // banned Assert.Null(session2Notif); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task FriendsShouldBeAddedAndAccepted() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket2 = Nakama.Socket.From(_client); await _socket.ConnectAsync(session); await socket2.ConnectAsync(session2); IApiNotification? session1Notif = null; _socket.ReceivedNotification += (IApiNotification notif) => { session1Notif = notif; }; IApiNotification? session2Notif = null; socket2.ReceivedNotification += (IApiNotification notif) => { session2Notif = notif; }; await _client.AddFriendsAsync(session, new string[]{session2.UserId}); await Task.Delay(1000); Assert.NotNull(session2Notif); var friendList = await _client.ListFriendsAsync(session, 1); // has sent invitation Assert.Single(friendList.Friends); await _client.AddFriendsAsync(session2, new string[]{session.UserId}); await Task.Delay(1000); Assert.NotNull(session1Notif); friendList = await _client.ListFriendsAsync(session, 0); // friends Assert.Single(friendList.Friends); } } } ================================================ FILE: Nakama.Tests/GroupTest.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using Xunit; namespace Nakama.Tests.Api { public class GroupTest { private IClient _client; private ISocket _socket; public GroupTest() { _client = TestsUtil.FromSettingsFile(); _socket = Nakama.Socket.From(_client); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateGroup() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var name = $"{Guid.NewGuid()}"; const string desc = "A group for Marvel super heroes."; const string avatarUrl = "http://graph.facebook.com/892489324234/picture?type=square"; const string langTag = "en_US"; const bool open = false; var group = await _client.CreateGroupAsync(session, name, desc, avatarUrl, langTag, open); Assert.NotNull(group); Assert.NotNull(group.Id); Assert.NotNull(group.CreateTime); Assert.NotNull(group.UpdateTime); Assert.Equal(1, group.EdgeCount); Assert.Equal(name, group.Name); Assert.Equal(desc, group.Description); Assert.Equal(avatarUrl, group.AvatarUrl); Assert.Equal(langTag, group.LangTag); Assert.Equal(open, group.Open); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateGroupDefault() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var name = $"{Guid.NewGuid()}"; var group = await _client.CreateGroupAsync(session, name); Assert.NotNull(group); Assert.NotNull(group.Id); Assert.NotNull(group.CreateTime); Assert.NotNull(group.UpdateTime); Assert.Null(group.AvatarUrl); Assert.Null(group.Description); Assert.Equal(1, group.EdgeCount); Assert.True(group.Open); Assert.Equal(name, group.Name); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotCreateGroup() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var name = $"{Guid.NewGuid()}"; await _client.CreateGroupAsync(session, name); var ex = await Assert.ThrowsAsync(() => _client.CreateGroupAsync(session, name)); Assert.Equal((int) HttpStatusCode.Conflict, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldListGroups() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); // Must create at least one group. await _client.CreateGroupAsync(session, $"{Guid.NewGuid()}"); var result = await _client.ListGroupsAsync(session); Assert.NotNull(result); Assert.NotEmpty(result.Groups); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldListGroupsNameFilter() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var name = $"{Guid.NewGuid()}"; await _client.CreateGroupAsync(session, name); var result = await _client.ListGroupsAsync(session, name, 1); Assert.NotNull(result); Assert.True(result.Groups.Count() == 1); Assert.True(result.Groups.Count(g => name.Equals(g.Name)) == 1); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldListGroupsFilterTwo() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var basename = $"{Guid.NewGuid()}"; var name1 = string.Concat(basename, "1"); await _client.CreateGroupAsync(session, name1); var name2 = string.Concat(basename, "2"); await _client.CreateGroupAsync(session, name2); // Filter on name with a wildcard. var result = await _client.ListGroupsAsync(session, string.Concat(basename, "%"), 2); Assert.NotNull(result); Assert.True(result.Groups.Count() == 2); Assert.True(result.Groups.Count(g => name1.Equals(g.Name) || name2.Equals(g.Name)) == 2); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldListGroupsCursor() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _client.CreateGroupAsync(session, $"{Guid.NewGuid()}"); await _client.CreateGroupAsync(session, $"{Guid.NewGuid()}"); var result = await _client.ListGroupsAsync(session); Assert.NotNull(result); Assert.NotNull(result.Cursor); result = await _client.ListGroupsAsync(session, null, 10, result.Cursor); Assert.NotNull(result); Assert.True(result.Groups.Count() >= 1); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldListGroupsByNameWithCursor() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); List expectedNames = new List(); for (var i = 0; i < 10; i++) { var name = $"Test{i.ToString()}"; try { await _client.CreateGroupAsync(session, name); } catch {} expectedNames.Add(name); } var page1 = await _client.ListGroupsAsync(session, "Tes%", 5); Assert.NotNull(page1); Assert.NotNull(page1.Cursor); Assert.Equal(5, page1.Groups.Count()); var idx = 0; foreach (var g in page1.Groups) { Assert.Equal(expectedNames[idx], g.Name); idx++; } var page2 = await _client.ListGroupsAsync(session, "Tes%", 5, page1.Cursor); Assert.NotNull(page2); Assert.Null(page2.Cursor); Assert.Equal(5, page2.Groups.Count()); foreach (var g in page2.Groups) { Assert.Equal(expectedNames[idx], g.Name); idx++; } } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldDeleteGroup() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var name = $"{Guid.NewGuid()}"; var group = await _client.CreateGroupAsync(session, name); await _client.DeleteGroupAsync(session, group.Id); var result1 = await _client.ListGroupsAsync(session, name); Assert.NotNull(result1); Assert.Empty(result1.Groups); var result2 = await _client.ListUserGroupsAsync(session); Assert.NotNull(result2); Assert.Empty(result2.UserGroups); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldDeleteGroupInvalid() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var ex = await Assert.ThrowsAsync(() => _client.DeleteGroupAsync(session, $"{Guid.NewGuid()}")); Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotDeleteGroupNotSuperAdmin() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var group = await _client.CreateGroupAsync(session1, $"{Guid.NewGuid()}"); var ex = await Assert.ThrowsAsync(() => _client.DeleteGroupAsync(session2, group.Id)); Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldPromoteAndDemoteUsers() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session3 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var group = await _client.CreateGroupAsync(session1, $"{Guid.NewGuid()}"); await _client.AddGroupUsersAsync(session1, group.Id, new string[]{session2.UserId, session3.UserId}); await _client.PromoteGroupUsersAsync(session1, group.Id, new string[]{session2.UserId, session3.UserId}); var admins = await _client.ListGroupUsersAsync(session1, group.Id, state: 1, limit: 2); Assert.Equal(2, admins.GroupUsers.Count()); await _client.DemoteGroupUsersAsync(session1, group.Id, new string[]{session2.UserId, session3.UserId}); admins = await _client.ListGroupUsersAsync(session1, group.Id, state: 1, limit: 2); Assert.Empty(admins.GroupUsers); var members = await _client.ListGroupUsersAsync(session1, group.Id, state: 2, limit: 2); Assert.Equal(2, members.GroupUsers.Count()); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldBanUsers() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session3 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var group = await _client.CreateGroupAsync(session1, $"{Guid.NewGuid()}"); await _client.AddGroupUsersAsync(session1, group.Id, new string[]{session2.UserId, session3.UserId}); await _client.BanGroupUsersAsync(session1, group.Id, new []{session2.UserId, session3.UserId}); var remainingMembers = await _client.ListGroupUsersAsync(session1, group.Id, state: null, limit: 100); Assert.Single(remainingMembers.GroupUsers); await _client.JoinGroupAsync(session2, group.Id); remainingMembers = await _client.ListGroupUsersAsync(session1, group.Id, state: null, limit: 100); Assert.Single(remainingMembers.GroupUsers); var groupList = await _client.ListUserGroupsAsync(session2, null, 100); Assert.Empty(groupList.UserGroups); } } } ================================================ FILE: Nakama.Tests/HttpErrorTest.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using Nakama.TinyJson; namespace Nakama.Tests.Api { using System; using System.Collections; using System.Threading.Tasks; using Xunit; // NOTE: Requires Lua modules from server repo. public class HttpErrorTest { private IClient _client; // ReSharper disable RedundantArgumentDefaultValue public HttpErrorTest() { _client = TestsUtil.FromSettingsFile(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task BadLuaRpcReturnsErrorMessageAndDict() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); const string funcid = "clientrpc.rpc_error"; _client.GlobalRetryConfiguration = null; var exception = await Assert.ThrowsAsync(() => _client.RpcAsync(session, funcid)); await Assert.ThrowsAsync(() => _client.RpcAsync(session, funcid)); Assert.NotNull(exception.Message); Assert.NotEmpty(exception.Message); var decoded = exception.Message.FromJson>(); Assert.Equal("Some error occured.", decoded["message"]); } [Fact(Skip = "requires go plugin")] public async Task BadGoRpcReturnsErrorMessageAndEmptyDict() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); const string funcid = "clientrpc.rpc_error_go"; var exception = await Assert.ThrowsAsync(() => _client.RpcAsync(session, funcid)); await Assert.ThrowsAsync(() => _client.RpcAsync(session, funcid)); Assert.NotNull(exception.Message); Assert.NotEmpty(exception.Message); Assert.Empty(exception.Data); } /* Make RPC calls to storage API as an example to test error format in Lua and Go runtimes. */ [Fact (Skip = "requires go plugin")] public async Task BadGoStorageRpcReturnsErrorMessageAndEmptyDict() { var session = await _client.AuthenticateCustomAsync("user_rpc_error_storage_go"); const string funcid = "clientrpc.rpc_error_storage_go"; var exception = await Assert.ThrowsAsync(() => _client.RpcAsync(session, funcid, session.UserId)); await Assert.ThrowsAsync(() => _client.RpcAsync(session, funcid)); Assert.NotNull(exception.Message); Assert.NotEmpty(exception.Message); // go runtime returns an empty object Assert.Empty(exception.Data); } } } ================================================ FILE: Nakama.Tests/LeaderboardAroundOwnerTest.cs ================================================ /** * Copyright 2021 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Xunit; namespace Nakama.Tests.Api { public class LeaderboardAroundOwnerTest : LeaderboardTest { private ISession[] _sessions = null; [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task OwnerInFront() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 10, limit: 4, ownerIndex: 0); var recordArray = records.Records.ToArray(); Assert.Equal(4, recordArray.Length); Assert.Equal("109", recordArray[0].Score); Assert.Equal("108", recordArray[1].Score); Assert.Equal("107", recordArray[2].Score); Assert.Equal("106", recordArray[3].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task OwnerInBack() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 10, limit: 4, ownerIndex: 9); var recordArray = records.Records.ToArray(); Assert.Equal(4, recordArray.Length); Assert.Equal("103", recordArray[0].Score); Assert.Equal("102", recordArray[1].Score); Assert.Equal("101", recordArray[2].Score); Assert.Equal("100", recordArray[3].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task OwnerNearFront() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 10, limit: 4, ownerIndex: 1); var recordArray = records.Records.ToArray(); Assert.Equal(4, recordArray.Length); Assert.Equal("109", recordArray[0].Score); Assert.Equal("108", recordArray[1].Score); Assert.Equal("107", recordArray[2].Score); Assert.Equal("106", recordArray[3].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task OwnerNearBack() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 10, limit: 4, ownerIndex: 8); var recordArray = records.Records.ToArray(); Assert.Equal(4, recordArray.Length); Assert.Equal("103", recordArray[0].Score); Assert.Equal("102", recordArray[1].Score); Assert.Equal("101", recordArray[2].Score); Assert.Equal("100", recordArray[3].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task OwnerInMiddle() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 10, limit: 4, ownerIndex: 5); var recordArray = records.Records.ToArray(); Assert.Equal(4, recordArray.Length); // owner score is 104 Assert.Equal("105", recordArray[0].Score); Assert.Equal("104", recordArray[1].Score); Assert.Equal("103", recordArray[2].Score); Assert.Equal("102", recordArray[3].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task NotEnoughRecordsForLimit() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 4, limit: 10, ownerIndex: 2); var recordArray = records.Records.ToArray(); Assert.Equal(4, recordArray.Length); Assert.Equal("103", recordArray[0].Score); Assert.Equal("102", recordArray[1].Score); Assert.Equal("101", recordArray[2].Score); Assert.Equal("100", recordArray[3].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task OddLimit() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 5, limit: 3, ownerIndex: 3); var recordArray = records.Records.ToArray(); Assert.Equal(3, recordArray.Length); // owner score is 101 Assert.Equal("102", recordArray[0].Score); Assert.Equal("101", recordArray[1].Score); Assert.Equal("100", recordArray[2].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task NoRecords() { await Assert.ThrowsAsync(() => CreateAndFetchRecords(numRecords: 1, limit: 0, ownerIndex: 0)); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task OneRecordOneLimit() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 1, limit: 1, ownerIndex: 0); var recordArray = records.Records.ToArray(); Assert.Single(recordArray); Assert.Equal("100", recordArray[0].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task TwoRecordsTwoLimit() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 2, limit: 2, ownerIndex: 1); var recordArray = records.Records.ToArray(); Assert.Equal(2, recordArray.Length); Assert.Equal("101", recordArray[0].Score); Assert.Equal("100", recordArray[1].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ThreeRecordsTwoLimit() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 3, limit: 2, ownerIndex: 1); var recordArray = records.Records.ToArray(); Assert.Equal(2, recordArray.Length); Assert.Equal("101", recordArray[0].Score); Assert.Equal("100", recordArray[1].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ThreeRecordsThreeLimit() { IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 3, limit: 3, ownerIndex: 1); var recordArray = records.Records.ToArray(); Assert.Equal(3, recordArray.Length); Assert.Equal("102", recordArray[0].Score); Assert.Equal("101", recordArray[1].Score); Assert.Equal("100", recordArray[2].Score); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task TestCursorsRecordInTheMiddle() { var ownerIndex = 1; IApiLeaderboardRecordList records = await CreateAndFetchRecords(numRecords: 3, limit: 1, ownerIndex: ownerIndex); var recordArray = records.Records.ToArray(); Assert.Equal(1, recordArray.Length); Assert.Equal("101", recordArray[0].Score); Assert.NotNull(records.NextCursor); Assert.NotNull(records.PrevCursor); var nextRecords = await _client.ListLeaderboardRecordsAroundOwnerAsync(_sessions[ownerIndex], _leaderboardId, _sessions[ownerIndex].UserId, null, 1, records.NextCursor); recordArray = nextRecords.Records.ToArray(); Assert.Single(recordArray); Assert.Equal("100", recordArray[0].Score); Assert.Null(nextRecords.NextCursor); Assert.NotNull(nextRecords.PrevCursor); var prevRecords = await _client.ListLeaderboardRecordsAroundOwnerAsync(_sessions[ownerIndex], _leaderboardId, _sessions[ownerIndex].UserId, null, 1, records.PrevCursor); recordArray = prevRecords.Records.ToArray(); Assert.Single(recordArray); Assert.Equal("102", recordArray[0].Score); Assert.NotNull(prevRecords.NextCursor); Assert.Null(prevRecords.PrevCursor); } private async Task CreateAndFetchRecords(int numRecords, int limit, int ownerIndex) { var authTasks = new List>(); for (int i = 0; i < numRecords; i++) { authTasks.Add(_client.AuthenticateCustomAsync($"{Guid.NewGuid()}")); } ISession[] sessions = await Task.WhenAll(authTasks.ToArray()); _sessions = sessions; var listTasks = new List>(); for (int i = 0; i < numRecords; i++) { int score = 100 + numRecords - i - 1; listTasks.Add(_client.WriteLeaderboardRecordAsync(sessions[i], _leaderboardId, score)); } Task.WaitAll(listTasks.ToArray()); IApiLeaderboardRecordList records = await _client.ListLeaderboardRecordsAroundOwnerAsync(sessions[ownerIndex], _leaderboardId, sessions[ownerIndex].UserId, null, limit); return records; } } } ================================================ FILE: Nakama.Tests/LeaderboardTest.cs ================================================ /** * Copyright 2021 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama.Tests.Api { using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Xunit; using TinyJson; public class LeaderboardTest : IAsyncLifetime { protected IClient _client; protected string _leaderboardId; // ReSharper disable RedundantArgumentDefaultValue public LeaderboardTest() { _client = TestsUtil.FromSettingsFile(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldWriteLeaderboardRecord() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); const long score = long.MaxValue; const int subscore = 10; const string metadata = "{\"race_conditions\": \"wet\"}"; var record = await _client.WriteLeaderboardRecordAsync(session, _leaderboardId, score, subscore, metadata); Assert.NotNull(record); Assert.NotEmpty(record.CreateTime); Assert.NotEmpty(record.UpdateTime); Assert.Equal(_leaderboardId, record.LeaderboardId); Assert.Equal(1, record.NumScore); Assert.Equal(score, long.Parse(record.Score)); Assert.Equal(subscore, long.Parse(record.Subscore)); Assert.Equal(session.UserId, record.OwnerId); Assert.Equal(session.Username, record.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldListLeaderboardRecordsWithOwnerId() { string guid = Guid.NewGuid().ToString(); string username = guid + "_username"; var session = await _client.AuthenticateCustomAsync(guid, username); await _client.WriteLeaderboardRecordAsync(session, _leaderboardId, 10L); var result = await _client.ListLeaderboardRecordsAsync(session, _leaderboardId, new[] {session.UserId}); Assert.NotNull(result); Assert.Null(result.NextCursor); Assert.Null(result.PrevCursor); Assert.NotEmpty(result.Records); Assert.Equal(1, result.OwnerRecords.Count(r => r.OwnerId == session.UserId)); Assert.Contains(result.Records, record => record.Username == username); Assert.Contains(result.OwnerRecords, record => record.Username == username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldListLeaderboardRecordsEmpty() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var result = await _client.ListLeaderboardRecordsAsync(session, _leaderboardId); Assert.NotNull(result); Assert.Null(result.NextCursor); Assert.Null(result.PrevCursor); Assert.Empty(result.Records); Assert.Empty(result.OwnerRecords); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldDeleteLeaderboardRecord() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _client.WriteLeaderboardRecordAsync(session, _leaderboardId, 10L); await _client.DeleteLeaderboardRecordAsync(session, _leaderboardId); var result = await _client.ListLeaderboardRecordsAsync(session, _leaderboardId, null, 100); Assert.NotNull(result); Assert.Null(result.NextCursor); Assert.Null(result.PrevCursor); Assert.Empty(result.Records); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldDeleteLeaderboardRecordNotFound() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _client.DeleteLeaderboardRecordAsync(session, _leaderboardId); } [Fact (Skip = "investigate this!")] public async Task ShouldDeleteLeaderboardRecordNotExists() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _client.DeleteLeaderboardRecordAsync(session, "invalid"); } public async Task InitializeAsync() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); // Must create a leaderboard. var payload = new Dictionary { {"operator", "best"} }.ToJson(); var rpc = await _client.RpcAsync(session, "clientrpc.create_leaderboard", payload); _leaderboardId = rpc.Payload.FromJson>()["leaderboard_id"]; } public Task DisposeAsync() { return Task.CompletedTask; } } } ================================================ FILE: Nakama.Tests/LinkUnlinkTest.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama.Tests.Api { using System; using System.Linq; using System.Net; using System.Threading.Tasks; using Xunit; public class LinkUnlinkTest { private IClient _client; public LinkUnlinkTest() { _client = TestsUtil.FromSettingsFile(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldLinkCustomId() { var customid1 = Guid.NewGuid().ToString(); var original = await _client.AuthenticateCustomAsync(customid1); var customid2 = Guid.NewGuid().ToString(); await _client.LinkCustomAsync(original, customid2); var updated = await _client.AuthenticateCustomAsync(customid2); Assert.NotNull(original); Assert.NotNull(updated); Assert.Equal(original.UserId, updated.UserId); Assert.Equal(original.Username, updated.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldLinkCustomIdSame() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); await _client.LinkCustomAsync(session, customid); var account = await _client.GetAccountAsync(session); Assert.NotNull(account); Assert.Equal(session.UserId, account.User.Id); Assert.Equal(session.Username, account.User.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldLinkCustomIdFieldEmpty() { var deviceid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateDeviceAsync(deviceid); var customid = Guid.NewGuid().ToString(); await _client.LinkCustomAsync(session, customid); var account = await _client.GetAccountAsync(session); Assert.NotNull(account); Assert.Equal(1, account.Devices.Count(d => d.Id == deviceid)); Assert.Equal(customid, account.CustomId); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldUnlinkCustomId() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var deviceid = Guid.NewGuid().ToString(); await _client.LinkDeviceAsync(session, deviceid); await _client.UnlinkCustomAsync(session, customid); var account = await _client.GetAccountAsync(session); Assert.NotNull(account); Assert.Single(account.Devices, d => d.Id == deviceid); Assert.Null(account.CustomId); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotLinkCustomIdInuse() { var customid = Guid.NewGuid().ToString(); await _client.AuthenticateCustomAsync(customid); var deviceid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateDeviceAsync(deviceid); var ex = await Assert.ThrowsAsync(() => _client.LinkCustomAsync(session, customid)); Assert.Equal((int) HttpStatusCode.Conflict, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkCustomId() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.UnlinkCustomAsync(session, customid)); Assert.Equal((int) HttpStatusCode.Forbidden, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkCustomIdNotOwned() { var customid = Guid.NewGuid().ToString(); await _client.AuthenticateCustomAsync(customid); var deviceid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateDeviceAsync(deviceid); var ex = await Assert.ThrowsAsync(() => _client.UnlinkCustomAsync(session, customid)); Assert.Equal((int) HttpStatusCode.Forbidden, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldLinkDeviceId() { var deviceid1 = Guid.NewGuid().ToString(); var original = await _client.AuthenticateDeviceAsync(deviceid1); var deviceid2 = Guid.NewGuid().ToString(); await _client.LinkDeviceAsync(original, deviceid2); var updated = await _client.AuthenticateDeviceAsync(deviceid2); Assert.NotNull(original); Assert.NotNull(updated); Assert.Equal(original.UserId, updated.UserId); Assert.Equal(original.Username, updated.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldLinkDeviceIdSame() { var deviceid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateDeviceAsync(deviceid); await _client.LinkDeviceAsync(session, deviceid); var account = await _client.GetAccountAsync(session); Assert.NotNull(account); Assert.Equal(session.UserId, account.User.Id); Assert.Equal(session.Username, account.User.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotLinkDeviceIdInuse() { var deviceid = Guid.NewGuid().ToString(); await _client.AuthenticateDeviceAsync(deviceid); var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.LinkDeviceAsync(session, deviceid)); Assert.Equal((int) HttpStatusCode.Conflict, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldUnlinkDeviceId() { var deviceid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateDeviceAsync(deviceid); await _client.LinkDeviceAsync(session, Guid.NewGuid().ToString()); await _client.UnlinkDeviceAsync(session, deviceid); var account = await _client.GetAccountAsync(session); Assert.NotNull(account); Assert.Equal(0, account.Devices.Count(d => d.Id == deviceid)); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkDeviceId() { var deviceid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateDeviceAsync(deviceid); var ex = await Assert.ThrowsAsync(() => _client.UnlinkDeviceAsync(session, deviceid)); Assert.Equal((int) HttpStatusCode.Forbidden, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkDeviceIdNotOwned() { var deviceid1 = Guid.NewGuid().ToString(); await _client.AuthenticateDeviceAsync(deviceid1); var deviceid2 = Guid.NewGuid().ToString(); var session = await _client.AuthenticateDeviceAsync(deviceid2); var ex = await Assert.ThrowsAsync(() => _client.UnlinkDeviceAsync(session, deviceid1)); Assert.Equal((int) HttpStatusCode.Forbidden, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldLinkEmail() { var customid = Guid.NewGuid().ToString(); var original = await _client.AuthenticateCustomAsync(customid); var email = string.Format("{0}@{0}.com", Guid.NewGuid().ToString()); const string password = "newpassword"; await _client.LinkEmailAsync(original, email, password); var updated = await _client.AuthenticateEmailAsync(email, password); Assert.NotNull(original); Assert.NotNull(updated); Assert.Equal(original.UserId, updated.UserId); Assert.Equal(original.Username, updated.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldLinkEmailSame() { var email = string.Format("{0}@{0}.com", Guid.NewGuid().ToString()); const string password = "newpassword"; var session = await _client.AuthenticateEmailAsync(email, password); await _client.LinkEmailAsync(session, email, password); var account = await _client.GetAccountAsync(session); Assert.NotNull(account); Assert.Equal(session.UserId, account.User.Id); Assert.Equal(session.Username, account.User.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotLinkEmailInuse() { var email = string.Format("{0}@{0}.com", Guid.NewGuid().ToString()); const string password = "newpassword"; await _client.AuthenticateEmailAsync(email, password); var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.LinkEmailAsync(session, email, password)); Assert.Equal((int) HttpStatusCode.Conflict, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldUnlinkEmail() { var email = string.Format("{0}@{0}.com", Guid.NewGuid().ToString()); const string password = "newpassword"; var session = await _client.AuthenticateEmailAsync(email, password); var customid = Guid.NewGuid().ToString(); await _client.LinkCustomAsync(session, customid); await _client.UnlinkEmailAsync(session, email, password); var account = await _client.GetAccountAsync(session); Assert.NotNull(account); Assert.NotEqual(email, account.Email); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkEmail() { var email = string.Format("{0}@{0}.com", Guid.NewGuid().ToString()); const string password = "newpassword"; var session = await _client.AuthenticateEmailAsync(email, password); var ex = await Assert.ThrowsAsync(() => _client.UnlinkEmailAsync(session, email, password)); Assert.Equal((int) HttpStatusCode.Forbidden, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkEmailNotOwned() { var email = string.Format("{0}@{0}.com", Guid.NewGuid().ToString()); const string password = "newpassword"; await _client.AuthenticateEmailAsync(email, password); var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.UnlinkEmailAsync(session, email, password)); Assert.Equal((int) HttpStatusCode.Forbidden, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotLinkFacebook() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.LinkFacebookAsync(session, "invalid")); Assert.Equal((int) HttpStatusCode.Unauthorized, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkFacebook() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.UnlinkFacebookAsync(session, "invalid")); Assert.Equal((int) HttpStatusCode.Unauthorized, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotLinkGameCenter() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); const string bundleId = "a"; const string playerId = "b"; const string publicKeyUrl = "c"; const string salt = "d"; const string signature = "e"; const string timestamp = "f"; var ex = await Assert.ThrowsAsync(() => _client.LinkGameCenterAsync(session, bundleId, playerId, publicKeyUrl, salt, signature, timestamp)); Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotLinkGameCenterBadInput() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var bundleId = string.Empty; var playerId = string.Empty; var publicKeyUrl = string.Empty; var salt = string.Empty; var signature = string.Empty; var timestamp = string.Empty; var ex = await Assert.ThrowsAsync(() => _client.LinkGameCenterAsync(session, bundleId, playerId, publicKeyUrl, salt, signature, timestamp)); Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkGameCenterBadInput() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var bundleId = string.Empty; var playerId = string.Empty; var publicKeyUrl = string.Empty; var salt = string.Empty; var signature = string.Empty; var timestamp = string.Empty; var ex = await Assert.ThrowsAsync(() => _client.UnlinkGameCenterAsync(session, bundleId, playerId, publicKeyUrl, salt, signature, timestamp)); Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotLinkGoogle() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.LinkGoogleAsync(session, "invalid")); Assert.Equal((int) HttpStatusCode.Unauthorized, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkGoogle() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.UnlinkGoogleAsync(session, "invalid")); Assert.Equal((int) HttpStatusCode.Unauthorized, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotLinkSteam() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.LinkSteamAsync(session, "invalid", false)); Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkSteam() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.UnlinkSteamAsync(session, "invalid")); Assert.Equal((int) HttpStatusCode.Unauthorized, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotLinkApple() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.LinkAppleAsync(session, "invalid")); Assert.Equal((int) HttpStatusCode.BadRequest, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotUnlinkApple() { var customid = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(customid); var ex = await Assert.ThrowsAsync(() => _client.UnlinkAppleAsync(session, "invalid")); Assert.Equal((int) HttpStatusCode.Unauthorized, ex.StatusCode); } } } ================================================ FILE: Nakama.Tests/Nakama.Tests.csproj ================================================ net8.0 enable false runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all Always ================================================ FILE: Nakama.Tests/PresenceUtilTest.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Linq; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace Nakama.Tests.Socket { public class PresenceUtilTest { private readonly ITestOutputHelper _testOutputHelper; private readonly IClient _client; public PresenceUtilTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; _client = TestsUtil.FromSettingsFile(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldAddPresencesParty() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session); var createdParty = await socket1.CreatePartyAsync(true, false, 2); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); var partyJoinTcs = new TaskCompletionSource(); socket1.ReceivedPartyPresence += presenceEvent => { createdParty.UpdatePresences(presenceEvent); partyJoinTcs.SetResult(presenceEvent); }; await socket2.JoinPartyAsync(createdParty.Id); await partyJoinTcs.Task; Assert.Equal(2, createdParty.Presences.Count()); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldAddAndRemovePresencesMatch() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session); var createdMatch = await socket1.CreateMatchAsync(); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); var matchJoinTcs = new TaskCompletionSource(); Action matchPresenceHandler = presenceEvent => { createdMatch.UpdatePresences(presenceEvent); matchJoinTcs.SetResult(presenceEvent); }; socket1.ReceivedMatchPresence += matchPresenceHandler; await socket2.JoinMatchAsync(createdMatch.Id); await matchJoinTcs.Task; socket1.ReceivedMatchPresence -= matchPresenceHandler; Assert.Equal(1, createdMatch.Presences.Count()); var matchLeaveTcs = new TaskCompletionSource(); socket1.ReceivedMatchPresence += presenceEvent => { createdMatch.UpdatePresences(presenceEvent); matchLeaveTcs.SetResult(presenceEvent); }; await socket2.LeaveMatchAsync(createdMatch); await matchLeaveTcs.Task; socket1.ReceivedMatchPresence -= matchPresenceHandler; Assert.Equal(0, createdMatch.Presences.Count()); await socket1.CloseAsync(); await socket2.CloseAsync(); } } } ================================================ FILE: Nakama.Tests/RetryTest.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Linq; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using System; namespace Nakama.Tests { public class RetryTest { [Fact] public async void TransientHttpAdapter_ServerDefault_CreatesSession() { var adapterSchedule = new TransientAdapterResponseType[1] { TransientAdapterResponseType.ServerOk }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); ISession session = await client.AuthenticateCustomAsync("test_id"); Assert.NotNull(session); } [Fact] public async void RetryConfiguration_OneRetries_RetriesExactlyOnce() { var adapterSchedule = new TransientAdapterResponseType[2] { TransientAdapterResponseType.TransientError, TransientAdapterResponseType.ServerOk }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); int lastNumRetry = -1; RetryListener retryListener = (int numRetry, Retry retry) => { lastNumRetry = numRetry; }; var config = new RetryConfiguration(baseDelayMs: 10, maxRetries: 1, retryListener); client.GlobalRetryConfiguration = config; ISession session = await client.AuthenticateCustomAsync("test_id"); Assert.NotNull(session); Assert.Equal(1, lastNumRetry); } [Fact] public async void RetryConfiguration_FiveRetries_RetriesExactlyFiveTimes() { var adapterSchedule = new TransientAdapterResponseType[6] { TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.ServerOk }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); int lastNumRetry = -1; RetryListener retryListener = (int numRetry, Retry retry) => { lastNumRetry = numRetry; }; var config = new RetryConfiguration(baseDelayMs: 1, maxRetries: 5, retryListener); client.GlobalRetryConfiguration = config; Task sessionTask = client.AuthenticateCustomAsync("test_id"); ISession session = await sessionTask; Assert.NotNull(session); Assert.Equal(5, lastNumRetry); } [Fact] public async void RetryConfiguration_PastMaxRetries_ThrowsTaskCancelledException() { var adapterSchedule = new TransientAdapterResponseType[4] { TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); int lastNumRetry = 3; RetryListener retryListener = (int numRetry, Retry retry) => { lastNumRetry = numRetry; }; var config = new RetryConfiguration(baseDelayMs: 500, maxRetries: 3, retryListener); client.GlobalRetryConfiguration = config; Task sessionTask = client.AuthenticateCustomAsync("test_id"); await Assert.ThrowsAsync(async () => await sessionTask); Assert.Equal(3, lastNumRetry); } [Fact] public async void RetryConfiguration_ZeroRetries_RetriesZeroTimes() { var adapterSchedule = new TransientAdapterResponseType[1] { TransientAdapterResponseType.TransientError }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); int lastNumRetry = -1; RetryListener retryListener = (int numRetry, Retry retry) => { lastNumRetry = numRetry; }; var config = new RetryConfiguration(baseDelayMs: 10, maxRetries: 0, retryListener); client.GlobalRetryConfiguration = config; Task sessionTask = client.AuthenticateCustomAsync("test_id"); await Assert.ThrowsAsync(async () => await sessionTask); Assert.Equal(-1, lastNumRetry); } [Fact] public async void RetryConfiguration_OverrideSet_OverridesGlobal() { var adapterSchedule = new TransientAdapterResponseType[4] { TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.ServerOk }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); int lastNumRetry = -1; RetryListener retryListener = (int numRetry, Retry retry) => { lastNumRetry = numRetry; }; var globalConfig = new RetryConfiguration(baseDelayMs: 10, maxRetries: 1, retryListener); client.GlobalRetryConfiguration = globalConfig; var localConfig = new RetryConfiguration(baseDelayMs: 10, maxRetries: 3, retryListener); var session = await client.AuthenticateCustomAsync("test_id", null, true, null, localConfig); Assert.NotNull(session); Assert.Equal(3, lastNumRetry); } [Fact] public async void RetryConfiguration_Delay_ExpectedExponentialTimes() { var adapterSchedule = new TransientAdapterResponseType[4] { TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.ServerOk }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); var retries = new List(); RetryListener retryListener = (int numRetry, Retry retry) => { retries.Add(retry); }; var config = new RetryConfiguration(baseDelayMs: 10, maxRetries: 3, retryListener); client.GlobalRetryConfiguration = config; Task sessionTask = client.AuthenticateCustomAsync("test_id"); ISession session = await sessionTask; Assert.NotNull(session); Assert.Equal(10, retries[0].ExponentialBackoff); Assert.Equal(20, retries[1].ExponentialBackoff); Assert.Equal(40, retries[2].ExponentialBackoff); } [Fact] public async void RetryConfiguration_Delay_ExpectedDelays() { var adapterSchedule = new TransientAdapterResponseType[3] { TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); var retries = new List(); RetryListener retryListener = (int numRetry, Retry retry) => { retries.Add(retry); }; var config = new RetryConfiguration(baseDelayMs: 10, maxRetries: 3, retryListener); client.GlobalRetryConfiguration = config; DateTime timeBeforeRequest = DateTime.Now; DateTime timeAfterRequest = default(DateTime); try { await client.AuthenticateCustomAsync("test_id"); } catch { timeAfterRequest = DateTime.Now; } int expectedElapsedTime = retries.Sum(retry => retry.JitterBackoff); int actualElapsedTime = (int)(timeAfterRequest - timeBeforeRequest).TotalMilliseconds; // actual will be slightly higher due to cpu elapsed time Assert.True(expectedElapsedTime < actualElapsedTime); } [Fact] public async void RetryConfiguration_NullConfiguration_DoesNotThrowNullRef() { var adapterSchedule = new TransientAdapterResponseType[3] { TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); client.GlobalRetryConfiguration = null; await Assert.ThrowsAsync(async () => await client.AuthenticateCustomAsync("test_id")); } [Fact] public async void RetryConfiguration_NoRetries_ThrowsBaseApiResponseException() { var adapterSchedule = new TransientAdapterResponseType[3] { TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, TransientAdapterResponseType.TransientError, }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); client.GlobalRetryConfiguration = new RetryConfiguration(baseDelayMs: 1, maxRetries: 0); try { await client.AuthenticateCustomAsync("test_id"); throw new Exception("Test failed due to not throwing an exception"); } catch (TaskCanceledException e) { Assert.True(e.GetBaseException() != null && e.GetBaseException() is ApiResponseException); } catch (Exception e) { throw e; } } [Fact] public async void RetryConfiguration_NonTransientError_Throws() { var adapterSchedule = new TransientAdapterResponseType[1] { TransientAdapterResponseType.NonTransientError, }; var adapter = new TransientExceptionHttpAdapter(adapterSchedule); var client = TestsUtil.FromSettingsFile(TestsUtil.DefaultSettingsPath, adapter); client.GlobalRetryConfiguration = new RetryConfiguration(baseDelayMs: 1, maxRetries: 3); ApiResponseException e = await Assert.ThrowsAsync(async () => await client.AuthenticateCustomAsync("test_id")); } } } ================================================ FILE: Nakama.Tests/RpcTest.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama.Tests.Api { using System; using System.Threading.Tasks; using Xunit; // NOTE: Requires Lua modules from server repo. public class RpcTest { private IClient _client; // ReSharper disable RedundantArgumentDefaultValue public RpcTest() { _client = TestsUtil.FromSettingsFile(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldRpcRoundtrip() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); const string funcid = "clientrpc.rpc"; const string payload = "{\"hello\": \"world\"}"; var rpc = await _client.RpcAsync(session, funcid, payload); Assert.NotNull(rpc); Assert.Equal(payload, rpc.Payload); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldRpcGet() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); const string funcid = "clientrpc.rpc_get"; var rpc = await _client.RpcAsync(session, funcid); Assert.NotNull(rpc); Assert.Equal("{\"message\":\"PONG\"}", rpc.Payload); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldRpcGetRoundtrip() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); const string funcid = "clientrpc.rpc"; const string payload = "{\"hello\": \"world\"}"; var rpc = await _client.RpcAsync(session, funcid, payload); Assert.NotNull(rpc); Assert.Equal(payload, rpc.Payload); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldRpcWithoutSession() { // Http Key is used often for server to server function calls const string httpkey = "defaulthttpkey"; const string funcid = "clientrpc.rpc_get"; var rpc = await _client.RpcAsync(httpkey, funcid); Assert.NotNull(rpc); Assert.Equal("{\"message\":\"PONG\"}", rpc.Payload); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldRpcGetRoundtripWithoutSession() { // Http Key is used often for server to server function calls const string httpkey = "defaulthttpkey"; const string funcid = "clientrpc.rpc"; const string payload = "{\"hello\": \"world\"}"; var rpc = await _client.RpcAsync(httpkey, funcid, payload); Assert.NotNull(rpc); Assert.Equal(payload, rpc.Payload); } } } ================================================ FILE: Nakama.Tests/SessionTest.cs ================================================ // Copyright 2018 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Linq; using Xunit; namespace Nakama.Tests { // NOTE Test name patterns are: MethodName_StateUnderTest_ExpectedBehavior public class SessionTest { private const string AuthToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTY5MTA5NzMsInVpZCI6ImY0MTU4ZjJiLTgwZjMtNDkyNi05NDZiLWE4Y2NmYzE2NTQ5MCIsInVzbiI6InZUR2RHSHl4dmwiLCJpYXQiOjE1MTY5MDczNzN9.01JtFdklpNfwHHCjItSGTbFBui3LyC3drqkrw6biy1I"; private const string AuthTokenVariables = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTY5MTA5NzMsInVpZCI6ImY0MTU4ZjJiLTgwZjMtNDkyNi05NDZiLWE4Y2NmYzE2NTQ5MCIsInVzbiI6InZUR2RHSHl4dmwiLCJ2cnMiOnsiazEiOiJ2MSIsImsyIjoidjIifX0.Hs9ltsNmtrTJXi2U21jjuXcd-3DMsyv4W6u1vyDBMTo"; private const string RefreshToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI1NTVjNDQwMC0yZGIxLTRkYmEtOTgwMC1jZjBmYzljMTVjMTAiLCJ1c24iOiJ1YWVuWGxFRnlhIiwiZXhwIjoxNjE2MzQ3OTc2fQ.l6bKhmcEbGHKV8YQVDKF8ysmWgOqcz3tCDSRn0eIKPw"; [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void GetVariables_VariablesField_Empty() { var session = Session.Restore(AuthToken); Assert.NotNull(session.AuthToken); Assert.Equal(AuthToken, session.AuthToken); Assert.NotNull(session.Vars); Assert.Empty(session.Vars); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void GetVariables_VariablesField_Values() { var session = Session.Restore(AuthTokenVariables); Assert.NotNull(session.AuthToken); Assert.NotNull(session.Username); Assert.Equal("vTGdGHyxvl", session.Username); Assert.NotNull(session.UserId); Assert.Equal("f4158f2b-80f3-4926-946b-a8ccfc165490", session.UserId); Assert.NotNull(session.Vars); Assert.Contains(session.Vars, pair => pair.Key.Equals("k1") || pair.Key.Equals("k2")); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void GetVariables_VariablesField_FromAuthenticate() { var client = TestsUtil.FromSettingsFile(); var id = Guid.NewGuid().ToString(); var vars = new Dictionary {{"k1", "v1"}}; var session = await client.AuthenticateDeviceAsync(id, null, true, vars); Assert.NotNull(session); Assert.NotNull(session.Vars); Assert.Equal(vars, session.Vars); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void GetRefreshToken_RefreshTokenField_FromAuthenticate() { var client = TestsUtil.FromSettingsFile(); var id = Guid.NewGuid().ToString(); var session = await client.AuthenticateDeviceAsync(id); Assert.NotNull(session); Assert.NotNull(session.RefreshToken); Assert.NotEqual(0L, session.RefreshExpireTime); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void SessionLogout_RefreshTokenField_Disabled() { var client = TestsUtil.FromSettingsFile(); var id = Guid.NewGuid().ToString(); var session = await client.AuthenticateDeviceAsync(id); Assert.NotNull(session); await client.SessionLogoutAsync(session); var ex = await Assert.ThrowsAsync(() => client.GetAccountAsync(session)); Assert.Equal(401, ex.StatusCode); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void GetUsername_UsernameField_NotNull() { var session = Session.Restore(AuthToken); Assert.NotNull(session.AuthToken); Assert.Equal(AuthToken, session.AuthToken); Assert.NotNull(session.Username); Assert.Equal("vTGdGHyxvl", session.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void GetUserId_UserIdField_NotNull() { var session = Session.Restore(AuthToken); Assert.NotNull(session.AuthToken); Assert.Equal(AuthToken, session.AuthToken); Assert.NotNull(session.UserId); Assert.Equal("f4158f2b-80f3-4926-946b-a8ccfc165490", session.UserId); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void IsExpired_ExpiredField_True() { var session = Session.Restore(AuthToken); Assert.NotNull(session.AuthToken); Assert.Equal(AuthToken, session.AuthToken); Assert.Equal(1516910973, session.ExpireTime); Assert.NotInRange(session.CreateTime, 0, 0); Assert.True(session.IsExpired); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void IsRefreshExpired_RefreshExpiredField_True() { var session = Session.Restore(AuthToken, RefreshToken); Assert.NotNull(session); Assert.Equal(RefreshToken, session.RefreshToken); Assert.Equal(1616347976, session.RefreshExpireTime); Assert.NotInRange(session.RefreshExpireTime, 0, 0); Assert.True(session.IsRefreshExpired); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void CreateTime_IsTokenIssField() { var session = Session.Restore(AuthToken); Assert.Equal(1516907373, session.CreateTime); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void Refresh_MetadataVar_DoesNotThrow() { var client = TestsUtil.FromSettingsFile(); const int numVars = 5; var initialVars = new Dictionary(); for (int i = 0; i < numVars; i++) { initialVars[$"{Guid.NewGuid()}"] = $"{Guid.NewGuid()}"; } var session = await client.AuthenticateCustomAsync("${Guid.NewGuid()}", null, true, initialVars); var newVars = new Dictionary(session.Vars); foreach (KeyValuePair var in newVars) { newVars[var.Key] = $"{Guid.NewGuid()}"; } session = await client.SessionRefreshAsync(session, newVars); Assert.Equal(session.Vars.Count, numVars); Assert.True(newVars.Keys.All(initialVars.Keys.Contains)); Assert.True(newVars.Values.All((val) => !initialVars.Values.Contains(val))); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void Restore_AuthTokenEmptyString_Null() { var session = Session.Restore(""); Assert.Null(session); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void Restore_RefreshTokenNull_Valid() { var session = Session.Restore(AuthToken, null); Assert.NotNull(session); Assert.Null(session.RefreshToken); Assert.Equal(0L, session.RefreshExpireTime); Assert.True(session.IsRefreshExpired); } } } ================================================ FILE: Nakama.Tests/Socket/WebSocketChannelTest.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama.Tests.Socket { using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using TinyJson; public class WebSocketChannelTest : IAsyncLifetime { private IClient _client; private ISocket _socket; public WebSocketChannelTest() { _client = TestsUtil.FromSettingsFile(); _socket = Nakama.Socket.From(_client); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateRoomChannel() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); var channel = await _socket.JoinChatAsync("myroom", ChannelType.Room); Assert.NotNull(channel); Assert.NotNull(channel.Id); Assert.Equal(channel.Self.UserId, session.UserId); Assert.Equal(channel.Self.Username, session.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldSendMessageRoomChannel() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var completer = new TaskCompletionSource(); _socket.ReceivedChannelMessage += (chatMessage) => completer.SetResult(chatMessage); await _socket.ConnectAsync(session); var channel = await _socket.JoinChatAsync("myroom", ChannelType.Room); // Send chat message. var content = new Dictionary {{"hello", "world"}}.ToJson(); var sendAck = await _socket.WriteChatMessageAsync(channel, content); var message = await completer.Task.ConfigureAwait(false); Assert.NotNull(sendAck); Assert.NotNull(message); Assert.Equal(sendAck.ChannelId, message.ChannelId); Assert.Equal(sendAck.MessageId, message.MessageId); Assert.Equal(sendAck.Username, message.Username); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldSendMessageDirectChannel() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _client.AddFriendsAsync(session1, new[] {session2.UserId}); await _client.AddFriendsAsync(session2, new[] {session1.UserId}); var completer = new TaskCompletionSource(); _socket.ReceivedChannelMessage += (chatMessage) => completer.SetResult(chatMessage); await _socket.ConnectAsync(session1); var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); var channel = await _socket.JoinChatAsync(session2.UserId, ChannelType.DirectMessage, false, false); // Send chat message. var content = new Dictionary {{"hello", "world"}}.ToJson(); var sendAck = await _socket.WriteChatMessageAsync(channel, content); var message = await completer.Task.ConfigureAwait(false); Assert.NotNull(sendAck); Assert.NotNull(message); Assert.Equal(sendAck.ChannelId, message.ChannelId); Assert.Equal(sendAck.MessageId, message.MessageId); Assert.Equal(sendAck.Username, message.Username); } Task IAsyncLifetime.InitializeAsync() { return Task.CompletedTask; } Task IAsyncLifetime.DisposeAsync() { return _socket.CloseAsync(); } } } ================================================ FILE: Nakama.Tests/Socket/WebSocketMatchTest.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Net.Sockets; namespace Nakama.Tests.Socket { using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Xunit; using TinyJson; // "Flakey. Needs improvement." public class WebSocketMatchTest : IAsyncLifetime { private IClient _client; private ISocket _socket; // ReSharper disable RedundantArgumentDefaultValue public WebSocketMatchTest() { _client = TestsUtil.FromSettingsFile(); _socket = Nakama.Socket.From(_client); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateMatch() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); var match = await _socket.CreateMatchAsync(); Assert.NotNull(match); Assert.NotNull(match.Id); Assert.NotEmpty(match.Id); Assert.False(match.Authoritative); Assert.True(match.Size > 0); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateMatchWithName() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); var match = await _socket.CreateMatchAsync("TestMatch"); Assert.NotNull(match); Assert.NotNull(match.Id); Assert.NotEmpty(match.Id); Assert.False(match.Authoritative); Assert.True(match.Size > 0); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldJoinMatchWithName() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); var match = await _socket.CreateMatchAsync("TestMatch"); // Currently MatchCreate is an upsert operation so there is no MatchJoin that accepts a name, calling MatchCreate with a match name // will return a deterministic match Id and place the user on the appropriate stream var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session2); var match2 = await _socket.CreateMatchAsync("TestMatch"); Assert.NotNull(match); Assert.NotNull(match2); Assert.Equal(match.Id, match2.Id); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateMatchAndSecondUserJoin() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session1); var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); var match1 = await _socket.CreateMatchAsync(); var match2 = await socket2.JoinMatchAsync(match1.Id); Assert.NotNull(match1); Assert.NotNull(match2); Assert.Equal(match1.Id, match2.Id); Assert.Equal(match1.Label, match2.Label); Assert.True(match1.Presences.Count() == 0 && match1.Self.UserId == session1.UserId); Assert.True(match2.Presences.Count() == 1); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateMatchAndLeave() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); var match = await _socket.CreateMatchAsync(); Assert.NotNull(match); Assert.NotNull(match.Id); await _socket.LeaveMatchAsync(match.Id); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateMatchAndSendState() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); _socket = Nakama.Socket.From(_client); await _socket.ConnectAsync(session1); var socket2 = Nakama.Socket.From(_client); var completer = new TaskCompletionSource(); socket2.ReceivedMatchState += (state) => completer.SetResult(state); await socket2.ConnectAsync(session2); var match = await _socket.CreateMatchAsync(); await socket2.JoinMatchAsync(match.Id); var newState = new Dictionary {{"hello", "world"}}.ToJson(); await _socket.SendMatchStateAsync(match.Id, 0, newState); var result = await completer.Task; Assert.NotNull(result); Assert.Equal(newState, Encoding.UTF8.GetString(result.State)); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task EachClientShouldReceiveTwoPresences() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket2 = Nakama.Socket.From(_client); HashSet socket1PresenceIds = new HashSet(); HashSet socket2PresenceIds = new HashSet(); _socket.ReceivedMatchPresence += (evt) => { foreach (string joinerId in evt.Joins.Select(joiner => joiner.UserId)) { socket1PresenceIds.Add(joinerId); } }; socket2.ReceivedMatchPresence += (evt) => { foreach (string joinerId in evt.Joins.Select(joiner => joiner.UserId)) { socket2PresenceIds.Add(joinerId); } }; await _socket.ConnectAsync(session); await socket2.ConnectAsync(session2); var match = await _socket.CreateMatchAsync(); var match2 = await socket2.JoinMatchAsync(match.Id); foreach (string existingId in match2.Presences.Select(joiner => joiner.UserId)) { socket2PresenceIds.Add(existingId); } await Task.Delay(1000); Assert.Equal(2, socket1PresenceIds.Count); Assert.Equal(2, socket2PresenceIds.Count); await _socket.LeaveMatchAsync(match.Id); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldThrowSocketExceptionWhenSendingMatchDataAfterClosingSocket() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); var match = await _socket.CreateMatchAsync(); await _socket.CloseAsync(); await Assert.ThrowsAsync(async () => { await _socket.SendMatchStateAsync(match.Id, 1, new { hello = "world" }.ToJson(), null); }); } Task IAsyncLifetime.InitializeAsync() { return Task.CompletedTask; } Task IAsyncLifetime.DisposeAsync() { return _socket.CloseAsync(); } } } ================================================ FILE: Nakama.Tests/Socket/WebSocketMatchmakerTest.cs ================================================ // Copyright 2020 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; namespace Nakama.Tests.Socket { public class WebSocketMatchmakerTest : IAsyncLifetime { private readonly IClient _client; private readonly ISocket _socket; public WebSocketMatchmakerTest() { _client = TestsUtil.FromSettingsFile(); _socket = Nakama.Socket.From(_client); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS_MATCHMAKER)] public async Task ShouldJoinMatchmaker() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); var matchmakerTicket = await _socket.AddMatchmakerAsync("*"); Assert.NotNull(matchmakerTicket); Assert.NotEmpty(matchmakerTicket.Ticket); } // "Flakey. Needs improvement." [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS_MATCHMAKER)] public async Task ShouldJoinAndLeaveMatchmaker() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); var matchmakerTicket = await _socket.AddMatchmakerAsync("*"); Assert.NotNull(matchmakerTicket); Assert.NotEmpty(matchmakerTicket.Ticket); await _socket.RemoveMatchmakerAsync(matchmakerTicket); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS_MATCHMAKER)] public async Task ShouldCompleteMatchmaker() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket2 = Nakama.Socket.From(_client); await _socket.ConnectAsync(session); await socket2.ConnectAsync(session2); var completer = new TaskCompletionSource(); var completer2 = new TaskCompletionSource(); _socket.ReceivedMatchmakerMatched += (state) => completer.SetResult(state); socket2.ReceivedMatchmakerMatched += (state) => completer2.SetResult(state); var matchmakerTicket = await _socket.AddMatchmakerAsync("*", 2, 2); await Task.Delay(2000); var matchmakerTicket2 = await socket2.AddMatchmakerAsync("*", 2, 2); Assert.NotNull(matchmakerTicket); Assert.NotEmpty(matchmakerTicket.Ticket); Assert.NotNull(matchmakerTicket2); Assert.NotEmpty(matchmakerTicket2.Ticket); var result = await completer.Task; var result2 = await completer2.Task; Assert.NotNull(result); Assert.NotNull(result2); Assert.NotEmpty(result.Token); Assert.NotEmpty(result2.Token); Assert.Equal(result.Token, result2.Token); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS_MATCHMAKER)] public async Task ShouldNotMatchPartiesWithACombinedAmountOfPlayersAboveMaxCount() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session3 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session4 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); var socket3 = Nakama.Socket.From(_client); var socket4 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); await socket3.ConnectAsync(session3); await socket4.ConnectAsync(session4); var party1PresenceJoinedTcs = new TaskCompletionSource(); socket1.ReceivedPartyPresence += presenceEvt => party1PresenceJoinedTcs.SetResult(presenceEvt); var party2PresenceJoinedTcs = new TaskCompletionSource(); socket3.ReceivedPartyPresence += presenceEvt => party2PresenceJoinedTcs.SetResult(presenceEvt); var mmCompleter1 = new TaskCompletionSource(); var mmCompleter2 = new TaskCompletionSource(); socket1.ReceivedMatchmakerMatched += (state) => mmCompleter1.SetResult(state); socket3.ReceivedMatchmakerMatched += (state) => mmCompleter2.SetResult(state); var party1 = await socket1.CreatePartyAsync(true, false, 2); var party2 = await socket3.CreatePartyAsync(true, false, 2); await socket2.JoinPartyAsync(party1.Id); await socket4.JoinPartyAsync(party2.Id); await party1PresenceJoinedTcs.Task; await party2PresenceJoinedTcs.Task; var addPartyResult1 = await socket1.AddMatchmakerPartyAsync(party1.Id, "*", 3, 3); var addPartyResult2 = await socket3.AddMatchmakerPartyAsync(party2.Id, "*", 3, 3); Assert.NotEmpty(addPartyResult1.Ticket); Assert.NotEmpty(addPartyResult2.Ticket); await Task.Delay(1000); Assert.False(mmCompleter1.Task.IsCompleted); Assert.False(mmCompleter2.Task.IsCompleted); await socket1.CloseAsync(); await socket2.CloseAsync(); await socket3.CloseAsync(); await socket4.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS_MATCHMAKER)] public async Task ShouldMatchPartiesWithPlayers() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session3 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); var socket3 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); await socket3.ConnectAsync(session3); var partyPresenceJoinedTcs = new TaskCompletionSource(); socket1.ReceivedPartyPresence += presenceEvt => partyPresenceJoinedTcs.SetResult(presenceEvt); var mmCompleter1 = new TaskCompletionSource(); var mmCompleter2 = new TaskCompletionSource(); socket1.ReceivedMatchmakerMatched += (state) => mmCompleter1.SetResult(state); socket3.ReceivedMatchmakerMatched += (state) => mmCompleter2.SetResult(state); var party1 = await socket1.CreatePartyAsync(true, false, 2); await socket2.JoinPartyAsync(party1.Id); await partyPresenceJoinedTcs.Task; var addPartyResult = await socket1.AddMatchmakerPartyAsync(party1.Id, "*", 3, 3); var addPlayerResult = await socket3.AddMatchmakerAsync( "*", 3, 3); Assert.NotEmpty(addPartyResult.Ticket); Assert.NotEmpty(addPlayerResult.Ticket); var partyMatchResult = await mmCompleter1.Task; var playerMatchResult = await mmCompleter2.Task; Assert.NotEmpty(partyMatchResult.Users); Assert.NotEmpty(playerMatchResult.Users); await socket1.CloseAsync(); await socket2.CloseAsync(); await socket3.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS_MATCHMAKER)] public async Task ShouldCompleteMatchmakerAsymmetricQuery() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket2 = Nakama.Socket.From(_client); await _socket.ConnectAsync(session); await socket2.ConnectAsync(session2); var completer = new TaskCompletionSource(); var completer2 = new TaskCompletionSource(); _socket.ReceivedMatchmakerMatched += (state) => completer.SetResult(state); socket2.ReceivedMatchmakerMatched += (state) => completer2.SetResult(state); var properties = new Dictionary(); properties.Add("code", "test1"); var properties2 = new Dictionary(); properties2.Add("code", "test2"); var matchmakerTicket = await _socket.AddMatchmakerAsync("properties.code:* properties.code:test1^5", 2, 2, properties); var matchmakerTicket2 = await socket2.AddMatchmakerAsync("*", 2, 2, properties2); Assert.NotNull(matchmakerTicket); Assert.NotEmpty(matchmakerTicket.Ticket); Assert.NotNull(matchmakerTicket2); Assert.NotEmpty(matchmakerTicket2.Ticket); await Task.Delay(1000); var result = await completer.Task; var result2 = await completer2.Task; Assert.NotNull(result); Assert.NotNull(result2); Assert.NotEmpty(result.Token); Assert.NotEmpty(result2.Token); Assert.Equal(result.Token, result2.Token); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS_MATCHMAKER)] public async Task ShouldCompleteMatchmakerSymmetricQueryMidSize() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session3 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket2 = Nakama.Socket.From(_client); var socket3 = Nakama.Socket.From(_client); await _socket.ConnectAsync(session); await socket2.ConnectAsync(session2); await socket3.ConnectAsync(session3); var completer = new TaskCompletionSource(); var completer2 = new TaskCompletionSource(); var completer3 = new TaskCompletionSource(); _socket.ReceivedMatchmakerMatched += (state) => completer.SetResult(state); socket2.ReceivedMatchmakerMatched += (state) => completer2.SetResult(state); socket3.ReceivedMatchmakerMatched += (state) => completer3.SetResult(state); var properties = new Dictionary(); properties.Add("foo", "bar"); var properties2 = new Dictionary(); properties2.Add("foo", "bar"); var properties3 = new Dictionary(); properties3.Add("foo", "bar"); var query = "+properties.foo:bar"; var query2 = "+properties.foo:bar"; var query3 = "+properties.foo:bar"; var matchmakerTicket = await _socket.AddMatchmakerAsync(query, 3, 3, properties); var matchmakerTicket2 = await socket2.AddMatchmakerAsync(query2, 3, 3, properties2); var matchmakerTicket3 = await socket3.AddMatchmakerAsync(query3, 3, 3, properties3); Assert.NotNull(matchmakerTicket); Assert.NotEmpty(matchmakerTicket.Ticket); Assert.NotNull(matchmakerTicket2); Assert.NotEmpty(matchmakerTicket2.Ticket); Assert.NotNull(matchmakerTicket3); Assert.NotEmpty(matchmakerTicket3.Ticket); await Task.Delay(3000); var result = await completer.Task; var result2 = await completer2.Task; var result3 = await completer3.Task; Assert.NotNull(result); Assert.NotNull(result2); Assert.NotNull(result3); Assert.NotEmpty(result.Token); Assert.NotEmpty(result2.Token); Assert.NotEmpty(result3.Token); Assert.Equal(result.Token, result2.Token); Assert.Equal(result2.Token, result3.Token); Assert.Equal(result.Token, result3.Token); } Task IAsyncLifetime.InitializeAsync() { return Task.CompletedTask; } Task IAsyncLifetime.DisposeAsync() { return _socket.CloseAsync(); } } } ================================================ FILE: Nakama.Tests/Socket/WebSocketNotificationTest.cs ================================================ /** * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Nakama.Tests.Socket { using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using TinyJson; using System.Linq; using System.Runtime.CompilerServices; public class WebSocketNotificationTest : IAsyncLifetime { private IClient _client; private ISocket _socket; public WebSocketNotificationTest() { _client = TestsUtil.FromSettingsFile(); _socket = Nakama.Socket.From(_client); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldReceiveNotification() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var completer = new TaskCompletionSource(); _socket.ReceivedNotification += (notification) => completer.SetResult(notification); await _socket.ConnectAsync(session); var payload = new Dictionary {{"user_id", session.UserId}}; var _ = _client.RpcAsync(session, "clientrpc.send_notification", payload.ToJson()); var result = await completer.Task; Assert.NotNull(result); Assert.Equal(session.UserId, result.SenderId); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldObtainDifferentCursors() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var payload = new Dictionary {{"user_id", session.UserId}}; for (int i = 0; i < 10; i++) { var _ = await _client.RpcAsync(session, "clientrpc.send_notification", payload.ToJson()); } IApiNotificationList notifs = await _client.ListNotificationsAsync(session, limit: 9); string firstCursor = notifs.CacheableCursor; Assert.Equal(9, notifs.Notifications.Count()); Assert.NotEmpty(firstCursor); notifs = await _client.ListNotificationsAsync(session, limit: 10, cacheableCursor: firstCursor); // should only be one left Assert.Single(notifs.Notifications); Assert.NotEmpty(notifs.CacheableCursor); Assert.NotEqual(firstCursor, notifs.CacheableCursor); } Task IAsyncLifetime.InitializeAsync() { return Task.CompletedTask; } Task IAsyncLifetime.DisposeAsync() { return _socket.CloseAsync(); } } } ================================================ FILE: Nakama.Tests/Socket/WebSocketPartyTest.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Linq; using System.Net.WebSockets; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; using System.Threading; using Nakama.TinyJson; namespace Nakama.Tests.Socket { public class WebSocketPartyTest { private readonly ITestOutputHelper _testOutputHelper; private readonly IClient _client; public WebSocketPartyTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; _client = TestsUtil.FromSettingsFile(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateParty() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket = Nakama.Socket.From(_client); await socket.ConnectAsync(session); var result = await socket.CreatePartyAsync(true, false, 1); Assert.NotNull(result); Assert.NotNull(result.Self); Assert.Equal(session.UserId, result.Self.UserId); Assert.Equal(session.Username, result.Self.Username); Assert.False(result.Hidden); Assert.True(result.Open); Assert.Equal(1, result.MaxSize); Assert.Null(result.Label); await socket.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreatePartyWithLabel() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket = Nakama.Socket.From(_client); await socket.ConnectAsync(session); var label = new Dictionary { { "team", "red" } }.ToJson(); var result = await socket.CreatePartyAsync(false, false, 1, label); Assert.NotNull(result); Assert.NotNull(result.Self); Assert.Equal(session.UserId, result.Self.UserId); Assert.Equal(session.Username, result.Self.Username); Assert.Equal(label, result.Label); await socket.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldReceiveJoinEvent() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket = Nakama.Socket.From(_client); await socket.ConnectAsync(session); var createdParty = await socket.CreatePartyAsync(true, false, 2); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); var partyReceivedTcs = new TaskCompletionSource(); socket2.JoinPartyAsync(createdParty.Id); socket2.ReceivedParty += party => { partyReceivedTcs.SetResult(party); }; var joinedParty = await partyReceivedTcs.Task; Assert.NotNull(joinedParty); Assert.NotNull(joinedParty.Self); Assert.Equal(session2.UserId, joinedParty.Self.UserId); Assert.Equal(session2.Username, joinedParty.Self.Username); await socket.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldAddAndRemovePartyFromMatchmaker() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); var partyJoinRequestTcs = new TaskCompletionSource(); socket1.ReceivedPartyJoinRequest += request => partyJoinRequestTcs.SetResult(request); var partyPresenceJoinedTcs = new TaskCompletionSource(); socket1.ReceivedPartyPresence += presenceEvt => partyPresenceJoinedTcs.SetResult(presenceEvt); var party = await socket1.CreatePartyAsync(false, false, 2); Assert.NotNull(party); Assert.NotEmpty(party.Id); Assert.False(party.Open); await socket2.JoinPartyAsync(party.Id); var joinRequest = await partyJoinRequestTcs.Task; await socket1.AcceptPartyMemberAsync(joinRequest.PartyId, joinRequest.Presences.First()); await partyPresenceJoinedTcs.Task; var result = await socket1.AddMatchmakerPartyAsync(party.Id, "*", 2, 2); Assert.NotEmpty(result.Ticket); await socket1.RemoveMatchmakerPartyAsync(party.Id, result.Ticket); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldPromoteMember() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); var partyJoinRequestTcs = new TaskCompletionSource(); socket1.ReceivedPartyJoinRequest += request => partyJoinRequestTcs.SetResult(request); var partyPromoteTcs = new TaskCompletionSource(); socket1.ReceivedPartyLeader += newLeader => partyPromoteTcs.SetResult(newLeader); var party = await socket1.CreatePartyAsync(false, false, 2); Assert.NotNull(party); Assert.NotEmpty(party.Id); Assert.False(party.Open); await socket2.JoinPartyAsync(party.Id); var joinRequest = await partyJoinRequestTcs.Task; _testOutputHelper.WriteLine(joinRequest.ToString()); var partyPresenceJoinedTcs = new TaskCompletionSource(); socket1.ReceivedPartyPresence += presenceEvt => partyPresenceJoinedTcs.SetResult(presenceEvt); await socket1.AcceptPartyMemberAsync(joinRequest.PartyId, joinRequest.Presences.First()); var partyPresenceEvent = await partyPresenceJoinedTcs.Task; _testOutputHelper.WriteLine(partyPresenceEvent.ToString()); await socket1.PromotePartyMemberAsync(party.Id, partyPresenceEvent.Joins.First()); var promotedLeader = await partyPromoteTcs.Task; _testOutputHelper.WriteLine(promotedLeader.ToString()); Assert.NotNull(promotedLeader); Assert.Equal(session2.UserId, promotedLeader.Presence.UserId); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldSendAndReceivePartyData() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); var party = await socket1.CreatePartyAsync(true, false, 2); await socket2.JoinPartyAsync(party.Id); var partyDataTcs = new TaskCompletionSource(); socket1.ReceivedPartyData += (data) => partyDataTcs.SetResult(data); await socket2.SendPartyDataAsync(party.Id, 0, System.Text.Encoding.UTF8.GetBytes("hello world")); await partyDataTcs.Task; Assert.Equal("hello world", System.Text.Encoding.UTF8.GetString(partyDataTcs.Task.Result.Data)); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldJoinClosedParty() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); var party = await socket1.CreatePartyAsync(false, false, 2); var requestedJoinTcs = new TaskCompletionSource(); socket1.ReceivedPartyJoinRequest += (request) => requestedJoinTcs.SetResult(request); await socket2.JoinPartyAsync(party.Id); await requestedJoinTcs.Task; var acceptedTcs = new TaskCompletionSource(); socket2.ReceivedParty += (party) => acceptedTcs.SetResult(party); foreach (var presence in requestedJoinTcs.Task.Result.Presences) { await socket1.AcceptPartyMemberAsync(requestedJoinTcs.Task.Result.PartyId, presence); } await acceptedTcs.Task; Assert.True(acceptedTcs.Task.Result.Id == party.Id); Assert.True(acceptedTcs.Task.Result.Self.UserId == session2.UserId); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldNotJoinPastMaxSize() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session3 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); var socket3 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); await socket3.ConnectAsync(session3); var party = await socket1.CreatePartyAsync(true, false, 2); await socket2.JoinPartyAsync(party.Id); await Assert.ThrowsAsync(() => socket3.JoinPartyAsync(party.Id)); await socket1.CloseAsync(); await socket2.CloseAsync(); await socket3.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task LeaderShouldBeInInitialPresences() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); var party = await socket1.CreatePartyAsync(true, false, 2); Assert.Single(party.Presences); Assert.Equal(party.Leader.UserId, party.Presences.First().UserId); await socket1.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task PresencesInitializedWithConcurrentJoins() { const int numMembers = 5; var leaderSession = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var leaderSocket = Nakama.Socket.From(_client); await leaderSocket.ConnectAsync(leaderSession); var memberSessions = new ISession[numMembers]; var memberSockets = new Nakama.ISocket[numMembers]; IParty party = await leaderSocket.CreatePartyAsync(true, false, numMembers + 1); var memberPartyObjects = new IParty[numMembers]; int partyObjCounter = 0; for (int i = 0; i < numMembers; i++) { memberSessions[i] = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); memberSockets[i] = (Nakama.Socket.From(_client)); await memberSockets[i].ConnectAsync(memberSessions[i]); memberSockets[i].ReceivedParty += party => { memberPartyObjects[partyObjCounter] = party; Interlocked.Increment(ref partyObjCounter); }; memberSockets[i].JoinPartyAsync(party.Id); } while (partyObjCounter < numMembers) { await Task.Delay(25); } // includes duplicates var combinedPresences = memberPartyObjects.SelectMany(party => party.Presences); foreach (var presence in combinedPresences) { Assert.False(string.IsNullOrEmpty(presence.UserId)); Assert.False(string.IsNullOrEmpty(presence.Username)); Assert.False(string.IsNullOrEmpty(presence.SessionId)); } await leaderSocket.CloseAsync(); foreach (var memberSocket in memberSockets) { await memberSocket.CloseAsync(); } } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldBootThenClose() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session3 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); var socket3 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); await socket3.ConnectAsync(session3); var party = await socket1.CreatePartyAsync(true, false, 3); var socket2PresenceTcs = new TaskCompletionSource(); socket1.ReceivedPartyPresence += presences => { var session2Join = presences.Joins.FirstOrDefault(presence => presence.UserId == session2.UserId); if (session2Join != null) { socket2PresenceTcs.SetResult(session2Join); } }; await socket2.JoinPartyAsync(party.Id); await socket3.JoinPartyAsync(party.Id); await socket2PresenceTcs.Task; var socket2CloseTcs = new TaskCompletionSource(); var socket3CloseTcs = new TaskCompletionSource(); socket2.ReceivedPartyClose += (close) => socket2CloseTcs.SetResult(); await socket1.RemovePartyMemberAsync(party.Id, socket2PresenceTcs.Task.Result); await socket2CloseTcs.Task; socket3.ReceivedPartyClose += (close) => socket3CloseTcs.SetResult(); await socket1.ClosePartyAsync(party.Id); await socket3CloseTcs.Task; await socket1.CloseAsync(); await socket2.CloseAsync(); await socket3.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task LeaderAndMembersShouldReceiveTicket() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); var party = await socket1.CreatePartyAsync(true, false, 2); await socket2.JoinPartyAsync(party.Id); var ticketTcs = new TaskCompletionSource(); socket2.ReceivedPartyMatchmakerTicket += (ticket) => ticketTcs.SetResult(ticket); var ticket = await socket1.AddMatchmakerPartyAsync(party.Id, "*", 2, 2); await ticketTcs.Task; Assert.Equal(ticketTcs.Task.Result.Ticket, ticket.Ticket); await socket1.RemoveMatchmakerPartyAsync(party.Id, ticket.Ticket); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldUpdateParty() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); var party = await socket1.CreatePartyAsync(true, false, 2); await socket2.JoinPartyAsync(party.Id); var updateTcs = new TaskCompletionSource(); socket2.ReceivedPartyUpdate += (update) => updateTcs.SetResult(update); var label = new Dictionary { { "mode", "test"}, { "one", 1 } }.ToJson(); await socket1.UpdatePartyAsync(party.Id, false, false, label); await updateTcs.Task; Assert.Equal(updateTcs.Task.Result.Label, label); Assert.False(updateTcs.Task.Result.Open); Assert.False(updateTcs.Task.Result.Hidden); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact (Timeout = TestsUtil.TIMEOUT_MILLISECONDS, Skip = "requires server configs --session.single_socket=true && --session.single_party=true")] public async Task SinglePartyShouldRemoveFromOtherParties() { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); var party = await socket1.CreatePartyAsync(true, false, 2); var socket2PresenceTcs = new TaskCompletionSource(); var socket2LeaveTcs = new TaskCompletionSource(); socket1.ReceivedPartyPresence += presences => { var session2Join = presences.Joins.FirstOrDefault(presence => presence.UserId == session2.UserId); if (session2Join != null) { socket2PresenceTcs.SetResult(session2Join); } var session2Leave = presences.Leaves.FirstOrDefault(presence => presence.UserId == session2.UserId); if (session2Leave != null) { socket2LeaveTcs.SetResult(session2Leave); } }; await socket2.JoinPartyAsync(party.Id); await socket2PresenceTcs.Task; await socket2.CreatePartyAsync(true, false, 2); await socket2LeaveTcs.Task; Assert.True(socket2PresenceTcs.Task.IsCompleted); Assert.True(socket2LeaveTcs.Task.IsCompleted); } } } ================================================ FILE: Nakama.Tests/Socket/WebSocketRpcTest.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Threading.Tasks; using Nakama.TinyJson; using Xunit; namespace Nakama.Tests.Socket { public class WebSocketRpcTest : IAsyncLifetime { private readonly IClient _client; private readonly ISocket _socket; public WebSocketRpcTest() { _client = TestsUtil.FromSettingsFile(); _socket = Nakama.Socket.From(_client); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldSendRpcRoundtrip() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); const string funcId = "clientrpc.rpc"; var payload = new Dictionary {{"hello", "world"}}.ToJson(); var response = await _socket.RpcAsync(funcId, payload); Assert.NotNull(response); Assert.Equal(funcId, response.Id); Assert.Equal(payload, response.Payload); } public Task InitializeAsync() => Task.CompletedTask; public Task DisposeAsync() => _socket.CloseAsync(); } } ================================================ FILE: Nakama.Tests/Socket/WebSocketTest.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace Nakama.Tests.Socket { public class WebSocketTest { private readonly ITestOutputHelper _testOutputHelper; private readonly IClient _client; private readonly ISocket _socket; // ReSharper disable RedundantArgumentDefaultValue public WebSocketTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; _client = TestsUtil.FromSettingsFile(); _socket = Nakama.Socket.From(_client, new WebSocketStdlibAdapter()); var logger = new StdoutLogger(); _socket.ReceivedError += e => logger.ErrorFormat(e.Message); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void ShouldCreateSocket() { var client = TestsUtil.FromSettingsFile(); var socket = Nakama.Socket.From(client); Assert.NotNull(socket); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateSocketAndConnect() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var completer = new TaskCompletionSource(); _socket.Connected += () => completer.SetResult(true); await _socket.ConnectAsync(session); Assert.True(await completer.Task); await _socket.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateSocketAndDisconnectEventListener() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var completer = new TaskCompletionSource(); _socket.Closed += (_) => completer.SetResult(true); await _socket.ConnectAsync(session); await _socket.CloseAsync(); Assert.True(await completer.Task); Assert.False(_socket.IsConnecting); Assert.False(_socket.IsConnected); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ShouldCreateSocketAndDisconnectSilent() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); Assert.True(_socket.IsConnected); await _socket.CloseAsync(); Assert.False(_socket.IsConnected); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task MultipleConnectAttemptsDoesNotThrowException() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session); Assert.True(_socket.IsConnected); await _socket.ConnectAsync(session); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async Task ClosingBeforeConnecting() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.CloseAsync(); await _socket.ConnectAsync(session); Assert.True(_socket.IsConnected); } [Fact(Skip = "Test case requires 60 seconds minimum execution time.")] public async Task LongLivedSocketLifecycle() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session, false, 5); await Task.Delay(TimeSpan.FromSeconds(60)); Assert.True(_socket.IsConnected); _ = _socket.CloseAsync(); } [Fact(Skip = "Test requires you to disconnect the internet and wait for 60 seconds minimum")] public async Task SocketDetectsLossOfInternet() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session, false, 5); var closeTriggered = false; _socket.Closed += (_) => { _testOutputHelper.WriteLine($"Socket was closed"); closeTriggered = true; }; _testOutputHelper.WriteLine("---Disconnect Internet Now---"); await Task.Delay(TimeSpan.FromSeconds(60)); Assert.False(_socket.IsConnected); Assert.True(closeTriggered); } [Fact] public async Task SocketCanReconnectAfterClose() { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); await _socket.ConnectAsync(session, false, 5); await _socket.CloseAsync(); await _socket.ConnectAsync(session, false, 5); var match = await _socket.CreateMatchAsync($"${Guid.NewGuid()}"); Assert.True(match != null); } } } ================================================ FILE: Nakama.Tests/Socket/WebSocketUserStatusTest.cs ================================================ /** * Copyright 2021 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Xunit; namespace Nakama.Tests.Socket { public class WebSocketUserStatusTest { private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); private IClient _client; public WebSocketUserStatusTest() { _client = TestsUtil.FromSettingsFile(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void FollowUsers_NoUsers_AnotherUser() { var id1 = Guid.NewGuid().ToString(); var id2 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var session2 = await _client.AuthenticateCustomAsync(id2); var completer = new TaskCompletionSource(); var socket1 = Nakama.Socket.From(_client); socket1.ReceivedStatusPresence += statuses => completer.SetResult(statuses); await socket1.ConnectAsync(session1); await socket1.FollowUsersAsync(new[] {session2.UserId}); var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); await socket2.UpdateStatusAsync("new status change"); var result = await completer.Task; Assert.NotNull(result); Assert.Contains(result.Joins, joined => joined.UserId.Equals(session2.UserId)); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void FollowUsers_NoUsers_AnotherUserByUsername() { var id = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id); var session2 = await _client.AuthenticateCustomAsync(id + "a"); var completer = new TaskCompletionSource(); var socket1 = Nakama.Socket.From(_client); socket1.ReceivedStatusPresence += statuses => completer.SetResult(statuses); socket1.ReceivedError += e => completer.TrySetException(e); await socket1.ConnectAsync(session1); await socket1.FollowUsersAsync(new string[] { }, new[] {session2.Username}); var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); await socket2.UpdateStatusAsync("new status change"); var result = await completer.Task; Assert.NotNull(result); Assert.Contains(result.Joins, joined => joined.UserId.Equals(session2.UserId)); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void FollowUsers_NoUsers_FollowedSelf() { var id = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(id); var socket1 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session); var statuses = await socket1.FollowUsersAsync(new[] {session.UserId}); Assert.NotNull(statuses); Assert.Empty(statuses.Presences); await socket1.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void FollowUsers_NoUsers_UserJoinsAndLeaves() { var id1 = Guid.NewGuid().ToString(); var id2 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var session2 = await _client.AuthenticateCustomAsync(id2); var completer1 = new TaskCompletionSource(); var socket1 = Nakama.Socket.From(_client); socket1.ReceivedStatusPresence += statuses => completer1.TrySetResult(statuses); socket1.ReceivedError += e => completer1.TrySetException(e); await socket1.ConnectAsync(session1); await socket1.FollowUsersAsync(new[] {session2.UserId}); // Second user comes online and sets status. var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); await socket2.UpdateStatusAsync("new status change"); var result1 = await completer1.Task; Assert.NotNull(result1); Assert.Empty(result1.Leaves); Assert.Contains(result1.Joins, joined => joined.UserId.Equals(session2.UserId)); var completer2 = new TaskCompletionSource(); socket1.ReceivedStatusPresence += statuses => completer2.SetResult(statuses); // Second user drops offline. await socket2.CloseAsync(); var result2 = await completer2.Task; Assert.NotNull(result2); Assert.Empty(result2.Joins); Assert.Contains(result2.Leaves, left => left.UserId.Equals(session2.UserId)); await socket1.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void FollowUsers_AlreadyOnline_HasStatus() { var id1 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var socket1 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); const string status1 = "test status"; await socket1.UpdateStatusAsync(status1); var id2 = Guid.NewGuid().ToString(); var session2 = await _client.AuthenticateCustomAsync(id2); var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); var statuses = await socket2.FollowUsersAsync(new[] {session1.UserId}); Assert.NotNull(statuses); Assert.Contains(statuses.Presences, presence => presence.Status.Equals("test status")); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void FollowUsers_TwoSessions_HasTwoStatuses() { var id1 = Guid.NewGuid().ToString(); var id2 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var session2 = await _client.AuthenticateCustomAsync(id2); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); // Both sockets for single user set statuses. const string status1 = "user 2 socket 1 status."; await socket1.UpdateStatusAsync(status1); const string status2 = "user 2 socket 2 status."; await socket2.UpdateStatusAsync(status2); var statuses = await socket1.FollowUsersAsync(new[] {session2.UserId}); Assert.NotNull(statuses); Assert.Contains(statuses.Presences, presence => presence.Status.Equals(status1) || presence.Status.Equals(status2)); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void FollowUsers_TwoUsers_ThirdUserFollowsBoth() { var id1 = Guid.NewGuid().ToString(); var socket1 = Nakama.Socket.From(_client); //socket1.ReceivedError var session1 = await _client.AuthenticateCustomAsync(id1); var id2 = Guid.NewGuid().ToString(); var socket2 = Nakama.Socket.From(_client); //socket2.ReceivedError var session2 = await _client.AuthenticateCustomAsync(id2); var id3 = Guid.NewGuid().ToString(); var socket3 = Nakama.Socket.From(_client); //socket3.ReceivedError var session3 = await _client.AuthenticateCustomAsync(id3); // Two users come online. Each publishes a status. await socket1.ConnectAsync(session1); await socket1.UpdateStatusAsync("user 1 status."); await socket2.ConnectAsync(session2); await socket2.UpdateStatusAsync("user 2 status."); // Third user comes online and follows both users. await socket3.ConnectAsync(session3); var statuses = await socket3.FollowUsersAsync(new[] {session1.UserId, session2.UserId}); Assert.NotNull(statuses); Assert.NotEmpty(statuses.Presences); Assert.Contains(statuses.Presences, presence => presence.UserId.Equals(session1.UserId) || presence.UserId.Equals(session2.UserId)); // Dispose await socket1.CloseAsync(); await socket2.CloseAsync(); await socket3.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void UpdateStatus_NoStatus_HasStatus() { var id = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(id); var completer = new TaskCompletionSource(); var socket1 = Nakama.Socket.From(_client); socket1.ReceivedStatusPresence += statuses => completer.SetResult(statuses); socket1.ReceivedError += e => completer.TrySetException(e); await socket1.ConnectAsync(session); await socket1.UpdateStatusAsync("super status change!"); var result = await completer.Task; Assert.NotNull(result); Assert.Contains(result.Joins, joined => joined.UserId.Equals(session.UserId)); await socket1.CloseAsync(); } [Fact (Skip = "Long-running test")] public async void TestFollowMassiveNumberOfUsers() { const int numFollowees = 500; var id1 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var socket1 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); var followeeTasks = new List(); var followeeSessions = new List(); for (int i = 0; i < numFollowees; i++) { ISocket socket = null; var followeeId = Guid.NewGuid().ToString(); var followeeTask = await _client.AuthenticateCustomAsync(followeeId) .ContinueWith(async session => { followeeSessions.Add(session.Result); socket = Nakama.Socket.From(_client); await socket.ConnectAsync(session.Result); await socket.UpdateStatusAsync("status for " + i.ToString()); }); followeeTasks.Add(followeeTask); } Task.WaitAll(followeeTasks.ToArray()); IStatus statuses = null; try { statuses = await socket1.FollowUsersAsync(followeeSessions.Select(session => session.UserId)); } catch (ApiResponseException) { throw; } Assert.Equal(numFollowees, statuses.Presences.Count()); await socket1.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void TestUserDoesNotReceiveUpdatedAfterUnfollow() { var id1 = Guid.NewGuid().ToString(); var id2 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var session2 = await _client.AuthenticateCustomAsync(id2); var waitForStatusPresence = new TaskCompletionSource(); var socket1 = Nakama.Socket.From(_client); Action receivedPresenceWhileFollowing = (statuses) => waitForStatusPresence.SetResult(statuses); socket1.ReceivedStatusPresence += receivedPresenceWhileFollowing; socket1.ReceivedError += e => { waitForStatusPresence.TrySetException(e); }; await socket1.ConnectAsync(session1); await socket1.FollowUsersAsync(new[] {session2.UserId}); var socket2 = Nakama.Socket.From(_client); await socket2.ConnectAsync(session2); await socket2.UpdateStatusAsync("new status change"); await waitForStatusPresence.Task; socket1.ReceivedStatusPresence -= receivedPresenceWhileFollowing; await socket1.UnfollowUsersAsync(new []{session2.UserId}); await socket2.UpdateStatusAsync("new status change that should not be received"); var ensureNoStatusPresence = new TaskCompletionSource(); socket1.ReceivedStatusPresence += status => { if (status.Joins.Any(join => join.UserId == session2.UserId)) { throw new Exception("Received user leave presence after unfollowing."); } }; await Task.Delay(Timeout); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void TestUserFollowSameUserTwice() { var id1 = Guid.NewGuid().ToString(); var id2 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var session2 = await _client.AuthenticateCustomAsync(id2); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); await socket1.FollowUsersAsync(new string[]{session2.UserId}); await socket1.FollowUsersAsync(new string[]{session2.UserId}); int numStatusesReceived = 0; socket1.ReceivedStatusPresence += status => { numStatusesReceived++; }; await socket2.UpdateStatusAsync("this should only be dispatched once"); await Task.Delay(Timeout); Assert.Equal(1, numStatusesReceived); await socket1.CloseAsync(); await socket2.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void TestUnfollowSelf() { var id1 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var socket1 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); bool receivedOwnPresence = false; socket1.ReceivedStatusPresence += status => { receivedOwnPresence = true; }; await socket1.UnfollowUsersAsync(new string[]{session1.UserId}); await socket1.UpdateStatusAsync("this should still be received by the user"); await Task.Delay(Timeout); Assert.True(receivedOwnPresence); await socket1.CloseAsync(); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public async void TestFollowNonExistentUser() { var id1 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var socket1 = Nakama.Socket.From(_client); await socket1.ConnectAsync(session1); await Assert.ThrowsAsync( () => socket1.FollowUsersAsync(new string[]{"does_not_exist"})); await socket1.CloseAsync(); } [Fact (Skip = "investigate this!")] public async void TestRepeatedOnlineOffline() { var id1 = Guid.NewGuid().ToString(); var id2 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); var session2 = await _client.AuthenticateCustomAsync(id2); var socket1 = Nakama.Socket.From(_client); var socket2 = Nakama.Socket.From(_client); int numJoinsReceived = 0; int numLeavesReceived = 0; socket1.ReceivedStatusPresence += status => { if (status.Joins.Any(join => join.UserId == session2.UserId)) { numJoinsReceived++; } if (status.Leaves.Any(leave => leave.UserId == session2.UserId)) { numLeavesReceived++; } }; await socket1.ConnectAsync(session1); await socket2.ConnectAsync(session2); await socket1.FollowUsersAsync(new string[]{session2.UserId}); await socket2.UpdateStatusAsync("I am going to spam socket 1 (first time)"); await socket2.CloseAsync(); await socket2.ConnectAsync(session2); await socket2.UpdateStatusAsync("I am going to spam socket 1 (second time)"); await socket2.CloseAsync(); await socket2.ConnectAsync(session2); await socket2.UpdateStatusAsync("I am going to spam socket 1 (third time)"); await socket2.CloseAsync(); await Task.Delay(Timeout); Assert.Equal(3, numLeavesReceived); Assert.Equal(3, numJoinsReceived); await socket1.CloseAsync(); } } } ================================================ FILE: Nakama.Tests/StdoutLogger.cs ================================================ // Copyright 2019 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Nakama.Tests { public class StdoutLogger : ILogger { public void DebugFormat(string format, params object[] args) { System.Console.WriteLine(string.Concat("[DEBUG] ", format), args); } public void ErrorFormat(string format, params object[] args) { System.Console.Error.WriteLine(string.Concat("[ERROR] ", format), args); } public void InfoFormat(string format, params object[] args) { System.Console.WriteLine(string.Concat("[INFO] ", format), args); } public void WarnFormat(string format, params object[] args) { System.Console.WriteLine(string.Concat("[WARN] ", format), args); } } } ================================================ FILE: Nakama.Tests/TestsUtil.cs ================================================ // Copyright 2021 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Microsoft.Extensions.Configuration; namespace Nakama.Tests { internal static class TestsUtil { public const int TIMEOUT_MILLISECONDS = 5000; public const int TIMEOUT_MILLISECONDS_MATCHMAKER = 15000; public const string DefaultSettingsPath = "settings.json"; public static IClient FromSettingsFile() { return FromSettingsFile(DefaultSettingsPath); } public static IClient FromSettingsFile(string path) { return FromSettingsFile(path, HttpRequestAdapter.WithGzip()); } public static IClient FromSettingsFile(string path, IHttpAdapter adapter) { var settings = new ConfigurationBuilder().AddJsonFile(path).Build(); var port = System.Convert.ToInt32(settings["PORT"]); var client = new Client(settings["SCHEME"], settings["HOST"], port, settings["SERVER_KEY"], adapter); if (System.Convert.ToBoolean(settings["STDOUT"])) { client.Logger = new StdoutLogger(); } return client; } } } ================================================ FILE: Nakama.Tests/TinyJsonParserTest.cs ================================================ /** * Copyright 2018 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using Nakama.TinyJson; using Xunit; namespace Nakama.Tests { public class TinyJsonParserTest { [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void FromJson_JsonInput_Parsed() { const string json = @"{""some_val"": ""val1"", ""nested"": [{""another_val"": ""val2""}]}"; ITestObject result = json.FromJson(); Assert.Equal("val1", result.SomeVal); Assert.Equal("val2", result.Nested.First().AnotherVal); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void FromJson_JsonInput_NumberToString() { const string json = @"{""key"":12345}"; var obj = json.FromJson>(); Assert.Equal("12345", obj["key"]); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void FromJson_JsonInput_SingleDigitNumberToString() { const string json = @"{""key"":1}"; var obj = json.FromJson>(); Assert.Equal("1", obj["key"]); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void FromJson_JsonInput_StringToString() { const string json = @"{""key"":""12345""}"; var obj = json.FromJson>(); Assert.Equal("12345", obj["key"]); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void ToJson_LongToUnquotedJson() { var obj = new Dictionary(); obj["key"] = 1234567891234; var json = obj.ToJson(); Assert.Equal("{\"key\":1234567891234}", json); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void FromJson_JsonInput_ParsedTwice() { const string json1 = @"{""some_val"": ""val1"", ""nested"": [{""another_val"": ""val2""}]}"; ITestObject result1 = json1.FromJson(); const string json2 = @"{""some_val"": ""val1"", ""nested"": [{""another_val"": ""val2""}]}"; ITestObject result2 = json2.FromJson(); Assert.Equal(result1.SomeVal, result2.SomeVal); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void FromJson_JsonInput_ParseSingleQuotesAsString() { const string json = @"{""key"":'foo'}"; var obj = json.FromJson>(); Assert.Equal("foo", obj["key"]); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void FromJson_JsonInput_ParseSingleQuotesAsStringInArray() { const string json = @"{""key"":['foo', 'bar']}"; var obj = json.FromJson>(); Assert.Equal(new [] { "foo", "bar" }, obj["key"]); } [Fact(Timeout = TestsUtil.TIMEOUT_MILLISECONDS)] public void FromJson_JsonInput_ParseBool() { const string json = @"{""key"":true}"; var obj = json.FromJson>(); Assert.Equal(true, obj["key"]); } } public interface ITestObject { string SomeVal { get; } IEnumerable Nested { get; } } internal class TestObject : ITestObject { [DataMember(Name="some_val")] public string SomeVal { get; set; } public IEnumerable Nested => _nested ?? new List(0); [DataMember(Name="nested")] // ReSharper disable once InconsistentNaming public List _nested { get; set; } } public interface INestedTestObject { string AnotherVal { get; } } internal class NestedTestObject : INestedTestObject { [DataMember(Name="another_val")] public string AnotherVal { get; set; } } } ================================================ FILE: Nakama.Tests/TransientExceptionHttpAdapter.cs ================================================ /** * Copyright 2021 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Nakama.Tests { public enum TransientAdapterResponseType { ServerOk, TransientError, NonTransientError } /// /// An adapter which throws transient/retryable exceptions whenever a request is made. /// public class TransientExceptionHttpAdapter : IHttpAdapter { public ILogger Logger { get; set; } public TransientExceptionDelegate TransientExceptionDelegate => IsTransientException; private int _sendAttempts = 0; private readonly TransientAdapterResponseType[] _sendSchedule; private readonly IHttpAdapter _httpRequestAdapter = HttpRequestAdapter.WithGzip(); public TransientExceptionHttpAdapter(TransientAdapterResponseType[] sendSchedule) { _sendSchedule = sendSchedule; } Task IHttpAdapter.SendAsync(string method, Uri uri, IDictionary headers, byte[] body, int timeoutSec, CancellationToken? userCancelToken) { if (_sendAttempts > _sendSchedule.Length - 1) { throw new IndexOutOfRangeException("The number of send attempts has exceeded the length of the send schedule."); } TransientAdapterResponseType responseType = _sendSchedule[_sendAttempts]; _sendAttempts++; switch (responseType) { case TransientAdapterResponseType.TransientError: throw new ApiResponseException(500, "This exception represents a transient error.", -1); case TransientAdapterResponseType.NonTransientError: throw new ApiResponseException(401, "This exception represents a non-transient error.", -1); default: return _httpRequestAdapter.SendAsync(method, uri, headers, body, timeoutSec); } } private bool IsTransientException(Exception e) { return (e is ApiResponseException apiException && apiException.StatusCode >= 500); } } } ================================================ FILE: Nakama.Tests/settings.json ================================================ { "HOST": "127.0.0.1", "PORT": 7350, "SCHEME": "http", "SERVER_KEY": "defaultkey", "STDOUT": false } ================================================ FILE: Nakama.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30114.105 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nakama", "Nakama\Nakama.csproj", "{A6440B73-D30E-4EAD-BC96-A834CE1DCC57}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nakama.Tests", "Nakama.Tests\Nakama.Tests.csproj", "{65EA41AD-C2EF-4F94-A466-927FAD7E8C1C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satori.Tests", "Satori.Tests\Satori.Tests.csproj", "{8801930E-55DC-43D8-841D-186E5F08B8C1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satori", "Satori\Satori.csproj", "{BD306F3C-4E6F-40C5-89BE-7071597BE626}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A6440B73-D30E-4EAD-BC96-A834CE1DCC57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A6440B73-D30E-4EAD-BC96-A834CE1DCC57}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6440B73-D30E-4EAD-BC96-A834CE1DCC57}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6440B73-D30E-4EAD-BC96-A834CE1DCC57}.Release|Any CPU.Build.0 = Release|Any CPU {65EA41AD-C2EF-4F94-A466-927FAD7E8C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {65EA41AD-C2EF-4F94-A466-927FAD7E8C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU {65EA41AD-C2EF-4F94-A466-927FAD7E8C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU {65EA41AD-C2EF-4F94-A466-927FAD7E8C1C}.Release|Any CPU.Build.0 = Release|Any CPU {8801930E-55DC-43D8-841D-186E5F08B8C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8801930E-55DC-43D8-841D-186E5F08B8C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {8801930E-55DC-43D8-841D-186E5F08B8C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {8801930E-55DC-43D8-841D-186E5F08B8C1}.Release|Any CPU.Build.0 = Release|Any CPU {BD306F3C-4E6F-40C5-89BE-7071597BE626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BD306F3C-4E6F-40C5-89BE-7071597BE626}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD306F3C-4E6F-40C5-89BE-7071597BE626}.Release|Any CPU.ActiveCfg = Release|Any CPU {BD306F3C-4E6F-40C5-89BE-7071597BE626}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal ================================================ FILE: README.md ================================================ Nakama .NET =========== # Nakama > .NET client for Nakama and Satori servers written in C#. [Nakama](https://github.com/heroiclabs/nakama) is an open-source server designed to power modern games and apps. Features include user accounts, chat, social, matchmaker, realtime multiplayer, and much [more](https://heroiclabs.com). [Satori](https://heroiclabs.com/satori/) is a LiveOps server that combines Event Capture, Segmentation, A/B Tests, Feature Flags, Events Calendar, and more together to provide Live Services gameplay. These clients implement the full API and socket options for their servers. All written in C# with minimal dependencies to support Unity, Xamarin, Godot, XNA, and other engines and frameworks. Full documentation is online - https://heroiclabs.com/docs ## Getting Started You'll need to setup the server and database before you can connect with the client. The simplest way is to use Docker but have a look at the [server documentation](https://github.com/heroiclabs/nakama#getting-started) for other options. 1. Install and run the servers. Follow these [instructions](https://heroiclabs.com/docs/install-docker-quickstart). 2. Download the client from the [releases page](https://github.com/heroiclabs/nakama-dotnet/releases) and import it into your project. You can also [build from source](#source-builds). 3. Use the connection credentials to build a client object. ```csharp // using Nakama; const string scheme = "http"; const string host = "127.0.0.1"; const int port = 7350; const string serverKey = "defaultkey"; var client = new Client(scheme, host, port, serverKey); ``` ## Usage The client object has many methods to execute various features in the server or open realtime socket connections with the server. ### Authenticate There's a variety of ways to [authenticate](https://heroiclabs.com/docs/authentication) with the server. Authentication can create a user if they don't already exist with those credentials. It's also easy to authenticate with a social profile from Google Play Games, Facebook, Game Center, etc. ```csharp var email = "super@heroes.com"; var password = "batsignal"; var session = await client.AuthenticateEmailAsync(email, password); System.Console.WriteLine(session); ``` ### Sessions When authenticated the server responds with an auth token (JWT) which contains useful properties and gets deserialized into a `Session` object. ```csharp System.Console.WriteLine(session.AuthToken); // raw JWT token System.Console.WriteLine(session.RefreshToken); // raw JWT token. System.Console.WriteLine(session.UserId); System.Console.WriteLine(session.Username); System.Console.WriteLine("Session has expired: {0}", session.IsExpired); System.Console.WriteLine("Session expires at: {0}", session.ExpireTime); ``` It is recommended to store the auth token from the session and check at startup if it has expired. If the token has expired you must reauthenticate. The expiry time of the token can be changed as a setting in the server. ```csharp var authToken = "restored from somewhere"; var refreshToken = "restored from somewhere"; var session = Session.Restore(authToken, refreshToken); // Check whether a session is close to expiry. if (session.HasExpired(DateTime.UtcNow.AddDays(1))) { try { session = await client.SessionRefreshAsync(session); } catch (ApiResponseException e) { System.Console.WriteLine("Session can no longer be refreshed. Must reauthenticate!"); } } ``` :warning: NOTE: The length of the lifetime of a session can be set on the server with the "--session.token_expiry_sec" command flag argument. The lifetime of the refresh token for a session can be set on the server with the "--session.refresh_token_expiry_sec" command flag. ### Requests The client includes lots of builtin APIs for various features of the game server. These can be accessed with the async methods. It can also call custom logic in RPC functions on the server. These can also be executed with a socket object. All requests are sent with a session object which authorizes the client. ```csharp var account = await client.GetAccountAsync(session); System.Console.WriteLine(account.User.Id); System.Console.WriteLine(account.User.Username); System.Console.WriteLine(account.Wallet); ``` Requests can be supplied with a retry configurations in cases of transient network or server errors. A single configuration can be used to control all request retry behavior: ```csharp var retryConfiguration = new RetryConfiguration(baseDelayMs: 1000, maxRetries: 5, delegate { System.Console.Writeline("about to retry."); }); client.GlobalRetryConfiguration = retryConfiguration; var account = await client.GetAccountAsync(session); ``` Or, the configuration can be supplied on a per-request basis: ```csharp var retryConfiguration = new RetryConfiguration(baseDelayMs: 1000, maxRetries: 5, delegate { System.Console.Writeline("about to retry."); }); var account = await client.GetAccountAsync(session, retryConfiguration); ``` Per-request retry configurations override the global retry configuration. Requests also can be supplied with a cancellation token if you need to cancel them mid-flight: ```csharp var canceller = new CancellationTokenSource(); var account = await client.GetAccountAsync(session, retryConfiguration: null, canceller); await Task.Delay(25); canceller.Cancel(); // will raise a TaskCanceledException ``` ### Socket The client can create one or more sockets with the server. Each socket can have it's own event listeners registered for responses received from the server. ```csharp var socket = Socket.From(client); socket.Connected += () => { System.Console.WriteLine("Socket connected."); }; socket.Closed += () => { System.Console.WriteLine("Socket closed."); }; socket.ReceivedError += e => System.Console.WriteLine(e); await socket.ConnectAsync(session); ``` # Satori Satori is a liveops server for games that powers actionable analytics, A/B testing and remote configuration. Use the Satori .NET Client to coomunicate with Satori from within your .NET game. Full documentation is online - https://heroiclabs.com/docs/satori/client-libraries/unity/index.html ## Getting Started Create a client object that accepts the API you were given as a Satori customer. ```csharp using Satori; const string scheme = "https"; const string host = "127.0.0.1"; // add your host here const int port = 443; const string apiKey = "apiKey"; // add the api key that was given to you as a Satori customer. var client = new Client(scheme, host, port, apiKey); ``` Then authenticate with the server to obtain your session. ```csharp // Authenticate with the Satori server. try { session = await client.AuthenticateAsync(id); Debug.Log("Authenticated successfully."); } catch(ApiResponseException ex) { Debug.LogFormat("Error authenticating: {0}", ex.Message); } ``` Using the client you can get any experiments or feature flags, the user belongs to. ```csharp var experiments = await client.GetExperimentsAsync(session); var flag = await client.GetFlagAsync(session, "FlagName"); ``` You can also send arbitrary events to the server: ```csharp await client.EventAsync(session, new Event("gameLaunched", DateTime.UtcNow)); ``` This is only a subset of the Satori client API, so please see the documentation link listed earlier for the [full API](https://dotnet.docs.heroiclabs.com/html/namespace_satori.html). # Contribute The development roadmap is managed as GitHub issues and pull requests are welcome. If you're interested to improve the code please open an issue to discuss the changes or drop in and discuss it in the [community forum](https://forum.heroiclabs.com). ## Source Builds The codebase can be built with the [Dotnet CLI](https://docs.microsoft.com/en-us/dotnet/core/tools). All dependencies are downloaded at build time with Nuget. ```shell dotnet build Nakama/Nakama.csproj ``` ```shell dotnet build Satori/Satori.csproj ``` For release builds see [our instructions](./RELEASEINST.md): ## Run Tests To run tests you will need to run the server and database. Most tests are written as integration tests which execute against the server. A quick approach we use with our test workflow is to use the Docker compose file described in the [documentation](https://heroiclabs.com/docs/install-docker-quickstart). ```shell docker-compose -f ./docker-compose-postgres.yml up dotnet test Nakama.Tests/Nakama.Tests.csproj ``` To run a specific test, pass the fully qualified name of the method to `dotnet test --filter`: ```shell dotnet test --filter "Nakama.Tests.Api.GroupTest.ShouldPromoteAndDemoteUsers" ``` If you'd like to attach a Visual Studio debugger to a test, set `VSTEST_HOST_DEBUG` to `true` in your shell environment and run `dotnet test`. Attach the debugger to the process identified by the console. In order to pass tests for Satori, the Satori console must be populated with sample data available via a button in its GUI. Then you can test the SDK with `dotnet test Satori.Tests/Satori.Tests.csproj`. ## Generate Codedocs The code documentation is generated with Doxygen and deployed to GitHub pages. You will need to install and add Doxygen tool to your system path (on macOS you can use `brew install doxygen`). ``` task -v codedocs ``` # Licenses This project is licensed under the [Apache-2 License](https://github.com/heroiclabs/nakama-dotnet/blob/master/LICENSE). # Special Thanks Thanks to Alex Parker (@zanders3) for the excellent [json](https://github.com/zanders3/json) library and David Haig (@ninjasource) for [Ninja.WebSockets](https://github.com/ninjasource/Ninja.WebSockets). ================================================ FILE: RELEASEINST.md ================================================ Release Instructions === This document outlines the release of the Nakama and Satori .NET clients to Github and [Nuget](https://www.nuget.org/packages/NakamaClient/). Our current monorepo strategy is to maintain the Nakama and Satori clients in the same repo per language. Clients are published together under Github under the same tag and version, even if only one has changed. Clients are released independently to Nuget. 1. Update and tidy up the CHANGELOG. 2. Run the test suite for the codebase. See the README for instructions. 3. Create a tag for the new release. This tag applies to both Nakama and Satori .NET: ```shell git add CHANGELOG git commit -m "Nakama .NET release." git tag -a -m "" git push origin master ``` 4. Build Nakama ```shell dotnet build -c Release ./Nakama/Nakama.csproj ``` 5. Build Satori ```shell dotnet build -c Release ./Satori/Satori.csproj ``` 6. Create a release on GitHub: https://github.com/heroiclabs/nakama-dotnet/releases/new 7. Copy the CHANGELOG section to the release notes. Upload the release DLLs to be part of the GitHub release. Publish it. 8. Package and push the releases to Nuget. Don't put a `v` prefix before the version number. ```shell dotnet pack -p:AssemblyVersion= -p:PackageVersion= -c Release ./Nakama/Nakama.csproj ``` ```shell dotnet nuget push ./Nakama/bin/Release/NakamaClient..nupkg -k "somekey" -s https://api.nuget.org/v3/index.json ``` ```shell dotnet pack -p:AssemblyVersion= -p:PackageVersion= -c Release ./Satori/Satori.csproj ``` ```shell dotnet nuget push ./Satori/bin/Release/SatoriClient..nupkg -k "somekey" -s https://api.nuget.org/v3/index.json ``` 9. Update CHANGELOG with section for new unreleased changes. ```shell git add CHANGELOG.md git commit -m "Set new development version." git push origin master ``` ================================================ FILE: Satori/ApiClient.gen.cs ================================================ /* Code generated by codegen/main.go. DO NOT EDIT. */ namespace Satori { using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; using TinyJson; /// /// An exception generated for HttpResponse objects don't return a success status. /// public sealed class ApiResponseException : Exception { public long StatusCode { get; } public int GrpcStatusCode { get; } public ApiResponseException(long statusCode, string content, int grpcCode) : base(content) { StatusCode = statusCode; GrpcStatusCode = grpcCode; } public ApiResponseException(string message, Exception e) : base(message, e) { StatusCode = -1L; GrpcStatusCode = -1; } public ApiResponseException(string content) : this(-1L, content, -1) { } public override string ToString() { return $"ApiResponseException(StatusCode={StatusCode}, Message='{Message}', GrpcStatusCode={GrpcStatusCode})"; } } /// /// The request to update the status of a message. /// public interface IApiUpdateMessageRequest { /// /// The time the message was consumed by the identity. /// string ConsumeTime { get; } /// /// The time the message was read at the client. /// string ReadTime { get; } } /// internal class ApiUpdateMessageRequest : IApiUpdateMessageRequest { /// [DataMember(Name="consume_time"), Preserve] public string ConsumeTime { get; set; } /// [DataMember(Name="read_time"), Preserve] public string ReadTime { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ConsumeTime: ", ConsumeTime, ", "); output = string.Concat(output, "ReadTime: ", ReadTime, ", "); return output; } } /// /// /// public interface IFlagValueChangeReason { /// /// The name of the configuration that overrides the flag value. /// string Name { get; } /// /// The type of the configuration that declared the override. /// FlagValueChangeReasonType Type { get; } /// /// The variant name of the configuration that overrides the flag value. /// string VariantName { get; } } /// internal class FlagValueChangeReason : IFlagValueChangeReason { /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [IgnoreDataMember] public FlagValueChangeReasonType Type => _type; [DataMember(Name="type"), Preserve] public FlagValueChangeReasonType _type { get; set; } /// [DataMember(Name="variant_name"), Preserve] public string VariantName { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "Type: ", Type, ", "); output = string.Concat(output, "VariantName: ", VariantName, ", "); return output; } } /// /// /// public enum FlagValueChangeReasonType { /// /// /// UNKNOWN = 0, /// /// /// FLAG_VARIANT = 1, /// /// /// LIVE_EVENT = 2, /// /// /// EXPERIMENT = 3, } /// /// Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. /// public interface IApiAuthenticateLogoutRequest { /// /// Refresh token to invalidate. /// string RefreshToken { get; } /// /// Session token to log out. /// string Token { get; } } /// internal class ApiAuthenticateLogoutRequest : IApiAuthenticateLogoutRequest { /// [DataMember(Name="refresh_token"), Preserve] public string RefreshToken { get; set; } /// [DataMember(Name="token"), Preserve] public string Token { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "RefreshToken: ", RefreshToken, ", "); output = string.Concat(output, "Token: ", Token, ", "); return output; } } /// /// Authenticate against the server with a refresh token. /// public interface IApiAuthenticateRefreshRequest { /// /// Refresh token. /// string RefreshToken { get; } } /// internal class ApiAuthenticateRefreshRequest : IApiAuthenticateRefreshRequest { /// [DataMember(Name="refresh_token"), Preserve] public string RefreshToken { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "RefreshToken: ", RefreshToken, ", "); return output; } } /// /// Authentication request /// public interface IApiAuthenticateRequest { /// /// Optional custom properties to update with this call. If not set, properties are left as they are on the server. /// IDictionary Custom { get; } /// /// Optional default properties to update with this call. If not set, properties are left as they are on the server. /// IDictionary Default { get; } /// /// Identity ID. Must be between eight and 128 characters (inclusive). Must be an alphanumeric string with only underscores and hyphens allowed. /// string Id { get; } /// /// Optional no_session modifies the request to only create/update an identity without creating a new session. If set to 'true' the response won't include a token and a refresh token. /// bool NoSession { get; } } /// internal class ApiAuthenticateRequest : IApiAuthenticateRequest { /// [IgnoreDataMember] public IDictionary Custom => _custom ?? new Dictionary(); [DataMember(Name="custom"), Preserve] public Dictionary _custom { get; set; } /// [IgnoreDataMember] public IDictionary Default => _default ?? new Dictionary(); [DataMember(Name="default"), Preserve] public Dictionary _default { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="no_session"), Preserve] public bool NoSession { get; set; } public override string ToString() { var output = ""; var customString = ""; foreach (var kvp in Custom) { customString = string.Concat(customString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Custom: [" + customString + "]"); var defaultString = ""; foreach (var kvp in Default) { defaultString = string.Concat(defaultString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Default: [" + defaultString + "]"); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "NoSession: ", NoSession, ", "); return output; } } /// /// A single event. Usually, but not necessarily, part of a batch. /// public interface IApiEvent { /// /// Optional event ID assigned by the client, used to de-duplicate in retransmission scenarios. If not supplied the server will assign a randomly generated unique event identifier. /// string Id { get; } /// /// The identity id associated with the event. Ignored if the event is published as part of a session. /// string IdentityId { get; } /// /// Event metadata, if any. /// IDictionary Metadata { get; } /// /// Event name. /// string Name { get; } /// /// The session expires at associated with the event. Ignored if the event is published as part of a session. /// string SessionExpiresAt { get; } /// /// The session id associated with the event. Ignored if the event is published as part of a session. /// string SessionId { get; } /// /// The session issued at associated with the event. Ignored if the event is published as part of a session. /// string SessionIssuedAt { get; } /// /// The time when the event was triggered on the producer side. /// string Timestamp { get; } /// /// Optional value. /// string Value { get; } } /// internal class ApiEvent : IApiEvent { /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="identity_id"), Preserve] public string IdentityId { get; set; } /// [IgnoreDataMember] public IDictionary Metadata => _metadata ?? new Dictionary(); [DataMember(Name="metadata"), Preserve] public Dictionary _metadata { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="session_expires_at"), Preserve] public string SessionExpiresAt { get; set; } /// [DataMember(Name="session_id"), Preserve] public string SessionId { get; set; } /// [DataMember(Name="session_issued_at"), Preserve] public string SessionIssuedAt { get; set; } /// [DataMember(Name="timestamp"), Preserve] public string Timestamp { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "IdentityId: ", IdentityId, ", "); var metadataString = ""; foreach (var kvp in Metadata) { metadataString = string.Concat(metadataString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Metadata: [" + metadataString + "]"); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "SessionExpiresAt: ", SessionExpiresAt, ", "); output = string.Concat(output, "SessionId: ", SessionId, ", "); output = string.Concat(output, "SessionIssuedAt: ", SessionIssuedAt, ", "); output = string.Concat(output, "Timestamp: ", Timestamp, ", "); output = string.Concat(output, "Value: ", Value, ", "); return output; } } /// /// Publish an event to the server /// public interface IApiEventRequest { /// /// Some number of events produced by a client. /// IEnumerable Events { get; } } /// internal class ApiEventRequest : IApiEventRequest { /// [IgnoreDataMember] public IEnumerable Events => _events ?? new List(0); [DataMember(Name="events"), Preserve] public List _events { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Events: [", string.Join(", ", Events), "], "); return output; } } /// /// An experiment that this user is partaking. /// public interface IApiExperiment { /// /// The labels associated with this experiment. /// List Labels { get; } /// /// Experiment name /// string Name { get; } /// /// Experiment Phase name /// string PhaseName { get; } /// /// Experiment Phase Variant name /// string PhaseVariantName { get; } /// /// Value associated with this Experiment. /// string Value { get; } } /// internal class ApiExperiment : IApiExperiment { /// [DataMember(Name="labels"), Preserve] public List Labels { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="phase_name"), Preserve] public string PhaseName { get; set; } /// [DataMember(Name="phase_variant_name"), Preserve] public string PhaseVariantName { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Labels: [", string.Join(", ", Labels), "], "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "PhaseName: ", PhaseName, ", "); output = string.Concat(output, "PhaseVariantName: ", PhaseVariantName, ", "); output = string.Concat(output, "Value: ", Value, ", "); return output; } } /// /// All experiments that this identity is involved with. /// public interface IApiExperimentList { /// /// All experiments for this identity. /// IEnumerable Experiments { get; } } /// internal class ApiExperimentList : IApiExperimentList { /// [IgnoreDataMember] public IEnumerable Experiments => _experiments ?? new List(0); [DataMember(Name="experiments"), Preserve] public List _experiments { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Experiments: [", string.Join(", ", Experiments), "], "); return output; } } /// /// Feature flag available to the identity. /// public interface IApiFlag { /// /// The origin of change on the flag value returned. /// IFlagValueChangeReason ChangeReason { get; } /// /// Whether the value for this flag has conditionally changed from the default state. /// bool ConditionChanged { get; } /// /// The labels associated with this flag. /// List Labels { get; } /// /// Flag name /// string Name { get; } /// /// Value associated with this flag. /// string Value { get; } } /// internal class ApiFlag : IApiFlag { /// [IgnoreDataMember] public IFlagValueChangeReason ChangeReason => _changeReason; [DataMember(Name="change_reason"), Preserve] public FlagValueChangeReason _changeReason { get; set; } /// [DataMember(Name="condition_changed"), Preserve] public bool ConditionChanged { get; set; } /// [DataMember(Name="labels"), Preserve] public List Labels { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ChangeReason: ", ChangeReason, ", "); output = string.Concat(output, "ConditionChanged: ", ConditionChanged, ", "); output = string.Concat(output, "Labels: [", string.Join(", ", Labels), "], "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "Value: ", Value, ", "); return output; } } /// /// All flags available to the identity /// public interface IApiFlagList { /// /// All flags /// IEnumerable Flags { get; } } /// internal class ApiFlagList : IApiFlagList { /// [IgnoreDataMember] public IEnumerable Flags => _flags ?? new List(0); [DataMember(Name="flags"), Preserve] public List _flags { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Flags: [", string.Join(", ", Flags), "], "); return output; } } /// /// Feature flag available to the identity. /// public interface IApiFlagOverride { /// /// Flag name /// string FlagName { get; } /// /// The labels associated with this flag. /// List Labels { get; } /// /// The list of configuration that affect the value of the flag. /// IEnumerable Overrides { get; } } /// internal class ApiFlagOverride : IApiFlagOverride { /// [DataMember(Name="flag_name"), Preserve] public string FlagName { get; set; } /// [DataMember(Name="labels"), Preserve] public List Labels { get; set; } /// [IgnoreDataMember] public IEnumerable Overrides => _overrides ?? new List(0); [DataMember(Name="overrides"), Preserve] public List _overrides { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "FlagName: ", FlagName, ", "); output = string.Concat(output, "Labels: [", string.Join(", ", Labels), "], "); output = string.Concat(output, "Overrides: [", string.Join(", ", Overrides), "], "); return output; } } /// /// All flags available to the identity and their value overrides /// public interface IApiFlagOverrideList { /// /// All flags /// IEnumerable Flags { get; } } /// internal class ApiFlagOverrideList : IApiFlagOverrideList { /// [IgnoreDataMember] public IEnumerable Flags => _flags ?? new List(0); [DataMember(Name="flags"), Preserve] public List _flags { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Flags: [", string.Join(", ", Flags), "], "); return output; } } /// /// /// public enum ApiFlagOverrideType { /// /// /// FLAG = 0, /// /// /// FLAG_VARIANT = 1, /// /// /// LIVE_EVENT_FLAG = 2, /// /// /// LIVE_EVENT_FLAG_VARIANT = 3, /// /// /// EXPERIMENT_PHASE_VARIANT_FLAG = 4, } /// /// The details of a flag value override. /// public interface IApiFlagOverrideValue { /// /// The create time of the configuration that overrides the flag. /// string CreateTimeSec { get; } /// /// The name of the configuration that overrides the flag value. /// string Name { get; } /// /// The type of the configuration that declared the override. /// ApiFlagOverrideType Type { get; } /// /// The value of the configuration that overrides the flag. /// string Value { get; } /// /// The variant name of the configuration that overrides the flag value. /// string VariantName { get; } } /// internal class ApiFlagOverrideValue : IApiFlagOverrideValue { /// [DataMember(Name="create_time_sec"), Preserve] public string CreateTimeSec { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [IgnoreDataMember] public ApiFlagOverrideType Type => _type; [DataMember(Name="type"), Preserve] public ApiFlagOverrideType _type { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } /// [DataMember(Name="variant_name"), Preserve] public string VariantName { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CreateTimeSec: ", CreateTimeSec, ", "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "Type: ", Type, ", "); output = string.Concat(output, "Value: ", Value, ", "); output = string.Concat(output, "VariantName: ", VariantName, ", "); return output; } } /// /// A response containing all the messages for an identity. /// public interface IApiGetMessageListResponse { /// /// Cacheable cursor to list newer messages. Durable and designed to be stored, unlike next/prev cursors. /// string CacheableCursor { get; } /// /// The list of messages. /// IEnumerable Messages { get; } /// /// The cursor to send when retrieving the next page, if any. /// string NextCursor { get; } /// /// The cursor to send when retrieving the previous page, if any. /// string PrevCursor { get; } } /// internal class ApiGetMessageListResponse : IApiGetMessageListResponse { /// [DataMember(Name="cacheable_cursor"), Preserve] public string CacheableCursor { get; set; } /// [IgnoreDataMember] public IEnumerable Messages => _messages ?? new List(0); [DataMember(Name="messages"), Preserve] public List _messages { get; set; } /// [DataMember(Name="next_cursor"), Preserve] public string NextCursor { get; set; } /// [DataMember(Name="prev_cursor"), Preserve] public string PrevCursor { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "CacheableCursor: ", CacheableCursor, ", "); output = string.Concat(output, "Messages: [", string.Join(", ", Messages), "], "); output = string.Concat(output, "NextCursor: ", NextCursor, ", "); output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); return output; } } /// /// Enrich/replace the current session with a new ID. /// public interface IApiIdentifyRequest { /// /// Optional custom properties to update with this call. If not set, properties are left as they are on the server. /// IDictionary Custom { get; } /// /// Optional default properties to update with this call. If not set, properties are left as they are on the server. /// IDictionary Default { get; } /// /// Identity ID to enrich the current session and return a new session. Old session will no longer be usable. /// string Id { get; } } /// internal class ApiIdentifyRequest : IApiIdentifyRequest { /// [IgnoreDataMember] public IDictionary Custom => _custom ?? new Dictionary(); [DataMember(Name="custom"), Preserve] public Dictionary _custom { get; set; } /// [IgnoreDataMember] public IDictionary Default => _default ?? new Dictionary(); [DataMember(Name="default"), Preserve] public Dictionary _default { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } public override string ToString() { var output = ""; var customString = ""; foreach (var kvp in Custom) { customString = string.Concat(customString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Custom: [" + customString + "]"); var defaultString = ""; foreach (var kvp in Default) { defaultString = string.Concat(defaultString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Default: [" + defaultString + "]"); output = string.Concat(output, "Id: ", Id, ", "); return output; } } /// /// A single live event. /// public interface IApiLiveEvent { /// /// End time of current event run. /// string ActiveEndTimeSec { get; } /// /// Start time of current event run. /// string ActiveStartTimeSec { get; } /// /// Description. /// string Description { get; } /// /// Duration in seconds. /// string DurationSec { get; } /// /// End time, 0 if it repeats forever. /// string EndTimeSec { get; } /// /// The live event identifier. /// string Id { get; } /// /// The labels associated with this live event. /// List Labels { get; } /// /// Name. /// string Name { get; } /// /// Reset CRON schedule, if configured. /// string ResetCron { get; } /// /// Start time. /// string StartTimeSec { get; } /// /// The status of this live event run. /// ApiLiveEventStatus Status { get; } /// /// Event value. /// string Value { get; } } /// internal class ApiLiveEvent : IApiLiveEvent { /// [DataMember(Name="active_end_time_sec"), Preserve] public string ActiveEndTimeSec { get; set; } /// [DataMember(Name="active_start_time_sec"), Preserve] public string ActiveStartTimeSec { get; set; } /// [DataMember(Name="description"), Preserve] public string Description { get; set; } /// [DataMember(Name="duration_sec"), Preserve] public string DurationSec { get; set; } /// [DataMember(Name="end_time_sec"), Preserve] public string EndTimeSec { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="labels"), Preserve] public List Labels { get; set; } /// [DataMember(Name="name"), Preserve] public string Name { get; set; } /// [DataMember(Name="reset_cron"), Preserve] public string ResetCron { get; set; } /// [DataMember(Name="start_time_sec"), Preserve] public string StartTimeSec { get; set; } /// [IgnoreDataMember] public ApiLiveEventStatus Status => _status; [DataMember(Name="status"), Preserve] public ApiLiveEventStatus _status { get; set; } /// [DataMember(Name="value"), Preserve] public string Value { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ActiveEndTimeSec: ", ActiveEndTimeSec, ", "); output = string.Concat(output, "ActiveStartTimeSec: ", ActiveStartTimeSec, ", "); output = string.Concat(output, "Description: ", Description, ", "); output = string.Concat(output, "DurationSec: ", DurationSec, ", "); output = string.Concat(output, "EndTimeSec: ", EndTimeSec, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "Labels: [", string.Join(", ", Labels), "], "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "ResetCron: ", ResetCron, ", "); output = string.Concat(output, "StartTimeSec: ", StartTimeSec, ", "); output = string.Concat(output, "Status: ", Status, ", "); output = string.Concat(output, "Value: ", Value, ", "); return output; } } /// /// List of Live events. /// public interface IApiLiveEventList { /// /// Live events that require explicit join. /// IEnumerable ExplicitJoinLiveEvents { get; } /// /// Live events. /// IEnumerable LiveEvents { get; } } /// internal class ApiLiveEventList : IApiLiveEventList { /// [IgnoreDataMember] public IEnumerable ExplicitJoinLiveEvents => _explicitJoinLiveEvents ?? new List(0); [DataMember(Name="explicit_join_live_events"), Preserve] public List _explicitJoinLiveEvents { get; set; } /// [IgnoreDataMember] public IEnumerable LiveEvents => _liveEvents ?? new List(0); [DataMember(Name="live_events"), Preserve] public List _liveEvents { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ExplicitJoinLiveEvents: [", string.Join(", ", ExplicitJoinLiveEvents), "], "); output = string.Concat(output, "LiveEvents: [", string.Join(", ", LiveEvents), "], "); return output; } } /// /// /// public enum ApiLiveEventStatus { /// /// The status variants of a live event. /// UNKNOWN = 0, /// /// /// ACTIVE = 1, /// /// /// UPCOMING = 2, /// /// /// TERMINATED = 3, } /// /// A scheduled message. /// public interface IApiMessage { /// /// The time the message was consumed by the identity. /// string ConsumeTime { get; } /// /// The time the message was created. /// string CreateTime { get; } /// /// The message's unique identifier. /// string Id { get; } /// /// The message's image url. /// string ImageUrl { get; } /// /// A key-value pairs of metadata. /// IDictionary Metadata { get; } /// /// The time the message was read by the client. /// string ReadTime { get; } /// /// The identifier of the schedule. /// string ScheduleId { get; } /// /// The send time for the message. /// string SendTime { get; } /// /// The message's text. /// string Text { get; } /// /// The message's title. /// string Title { get; } /// /// The time the message was updated. /// string UpdateTime { get; } } /// internal class ApiMessage : IApiMessage { /// [DataMember(Name="consume_time"), Preserve] public string ConsumeTime { get; set; } /// [DataMember(Name="create_time"), Preserve] public string CreateTime { get; set; } /// [DataMember(Name="id"), Preserve] public string Id { get; set; } /// [DataMember(Name="image_url"), Preserve] public string ImageUrl { get; set; } /// [IgnoreDataMember] public IDictionary Metadata => _metadata ?? new Dictionary(); [DataMember(Name="metadata"), Preserve] public Dictionary _metadata { get; set; } /// [DataMember(Name="read_time"), Preserve] public string ReadTime { get; set; } /// [DataMember(Name="schedule_id"), Preserve] public string ScheduleId { get; set; } /// [DataMember(Name="send_time"), Preserve] public string SendTime { get; set; } /// [DataMember(Name="text"), Preserve] public string Text { get; set; } /// [DataMember(Name="title"), Preserve] public string Title { get; set; } /// [DataMember(Name="update_time"), Preserve] public string UpdateTime { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "ConsumeTime: ", ConsumeTime, ", "); output = string.Concat(output, "CreateTime: ", CreateTime, ", "); output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "ImageUrl: ", ImageUrl, ", "); var metadataString = ""; foreach (var kvp in Metadata) { metadataString = string.Concat(metadataString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Metadata: [" + metadataString + "]"); output = string.Concat(output, "ReadTime: ", ReadTime, ", "); output = string.Concat(output, "ScheduleId: ", ScheduleId, ", "); output = string.Concat(output, "SendTime: ", SendTime, ", "); output = string.Concat(output, "Text: ", Text, ", "); output = string.Concat(output, "Title: ", Title, ", "); output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); return output; } } /// /// Properties associated with an identity. /// public interface IApiProperties { /// /// Event computed properties. /// IDictionary Computed { get; } /// /// Event custom properties. /// IDictionary Custom { get; } /// /// Event default properties. /// IDictionary Default { get; } } /// internal class ApiProperties : IApiProperties { /// [IgnoreDataMember] public IDictionary Computed => _computed ?? new Dictionary(); [DataMember(Name="computed"), Preserve] public Dictionary _computed { get; set; } /// [IgnoreDataMember] public IDictionary Custom => _custom ?? new Dictionary(); [DataMember(Name="custom"), Preserve] public Dictionary _custom { get; set; } /// [IgnoreDataMember] public IDictionary Default => _default ?? new Dictionary(); [DataMember(Name="default"), Preserve] public Dictionary _default { get; set; } public override string ToString() { var output = ""; var computedString = ""; foreach (var kvp in Computed) { computedString = string.Concat(computedString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Computed: [" + computedString + "]"); var customString = ""; foreach (var kvp in Custom) { customString = string.Concat(customString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Custom: [" + customString + "]"); var defaultString = ""; foreach (var kvp in Default) { defaultString = string.Concat(defaultString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Default: [" + defaultString + "]"); return output; } } /// /// A session. /// public interface IApiSession { /// /// Properties associated with this identity. /// IApiProperties Properties { get; } /// /// Refresh token. /// string RefreshToken { get; } /// /// Token credential. /// string Token { get; } } /// internal class ApiSession : IApiSession { /// [IgnoreDataMember] public IApiProperties Properties => _properties; [DataMember(Name="properties"), Preserve] public ApiProperties _properties { get; set; } /// [DataMember(Name="refresh_token"), Preserve] public string RefreshToken { get; set; } /// [DataMember(Name="token"), Preserve] public string Token { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Properties: ", Properties, ", "); output = string.Concat(output, "RefreshToken: ", RefreshToken, ", "); output = string.Concat(output, "Token: ", Token, ", "); return output; } } /// /// Update Properties associated with this identity. /// public interface IApiUpdatePropertiesRequest { /// /// Event custom properties. /// IDictionary Custom { get; } /// /// Event default properties. /// IDictionary Default { get; } /// /// Informs the server to recompute the audience membership of the identity. /// bool Recompute { get; } } /// internal class ApiUpdatePropertiesRequest : IApiUpdatePropertiesRequest { /// [IgnoreDataMember] public IDictionary Custom => _custom ?? new Dictionary(); [DataMember(Name="custom"), Preserve] public Dictionary _custom { get; set; } /// [IgnoreDataMember] public IDictionary Default => _default ?? new Dictionary(); [DataMember(Name="default"), Preserve] public Dictionary _default { get; set; } /// [DataMember(Name="recompute"), Preserve] public bool Recompute { get; set; } public override string ToString() { var output = ""; var customString = ""; foreach (var kvp in Custom) { customString = string.Concat(customString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Custom: [" + customString + "]"); var defaultString = ""; foreach (var kvp in Default) { defaultString = string.Concat(defaultString, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "Default: [" + defaultString + "]"); output = string.Concat(output, "Recompute: ", Recompute, ", "); return output; } } /// /// /// public interface IGooglerpcStatus { /// /// /// int Code { get; } /// /// /// IEnumerable Details { get; } /// /// /// string Message { get; } } /// internal class GooglerpcStatus : IGooglerpcStatus { /// [DataMember(Name="code"), Preserve] public int Code { get; set; } /// [IgnoreDataMember] public IEnumerable Details => _details ?? new List(0); [DataMember(Name="details"), Preserve] public List _details { get; set; } /// [DataMember(Name="message"), Preserve] public string Message { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "Code: ", Code, ", "); output = string.Concat(output, "Details: [", string.Join(", ", Details), "], "); output = string.Concat(output, "Message: ", Message, ", "); return output; } } /// /// /// public interface IProtobufAny { /// /// /// string @type { get; } } /// internal class ProtobufAny : IProtobufAny { /// [DataMember(Name="@type"), Preserve] public string @type { get; set; } public override string ToString() { var output = ""; output = string.Concat(output, "@type: ", @type, ", "); return output; } } /// /// The low level client for the Satori API. /// internal class ApiClient { public readonly IHttpAdapter HttpAdapter; public int Timeout { get; set; } private readonly Uri _baseUri; public ApiClient(Uri baseUri, IHttpAdapter httpAdapter, int timeout = 10) { _baseUri = baseUri; HttpAdapter = httpAdapter; Timeout = timeout; } /// /// A healthcheck which load balancers can use to check the service. /// public async Task SatoriHealthcheckAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/healthcheck"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// A readycheck which load balancers can use to check the service. /// public async Task SatoriReadycheckAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/readycheck"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Authenticate against the server. /// public async Task SatoriAuthenticateAsync( string basicAuthUsername, string basicAuthPassword, ApiAuthenticateRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v1/authenticate"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. /// public async Task SatoriAuthenticateLogoutAsync( string bearerToken, ApiAuthenticateLogoutRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v1/authenticate/logout"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Refresh a user's session using a refresh token retrieved from a previous authentication request. /// public async Task SatoriAuthenticateRefreshAsync( string basicAuthUsername, string basicAuthPassword, ApiAuthenticateRefreshRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v1/authenticate/refresh"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Publish an event for this session. /// public async Task SatoriEventAsync( string bearerToken, ApiEventRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v1/event"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Get or list all available experiments for this identity. /// public async Task SatoriGetExperimentsAsync( string bearerToken, IEnumerable names, IEnumerable labels, CancellationToken? cancellationToken) { var urlpath = "/v1/experiment"; var queryParams = ""; foreach (var elem in names ?? new string[0]) { queryParams = string.Concat(queryParams, "names=", Uri.EscapeDataString(elem), "&"); } foreach (var elem in labels ?? new string[0]) { queryParams = string.Concat(queryParams, "labels=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List all available flags for this identity. /// public async Task SatoriGetFlagsAsync( string basicAuthUsername, string basicAuthPassword, string bearerToken, IEnumerable names, IEnumerable labels, CancellationToken? cancellationToken) { var urlpath = "/v1/flag"; var queryParams = ""; foreach (var elem in names ?? new string[0]) { queryParams = string.Concat(queryParams, "names=", Uri.EscapeDataString(elem), "&"); } foreach (var elem in labels ?? new string[0]) { queryParams = string.Concat(queryParams, "labels=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } if (!string.IsNullOrEmpty(bearerToken)) { var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); } byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// List all available flags and their value overrides for this identity. /// public async Task SatoriGetFlagOverridesAsync( string basicAuthUsername, string basicAuthPassword, string bearerToken, IEnumerable names, IEnumerable labels, CancellationToken? cancellationToken) { var urlpath = "/v1/flag/override"; var queryParams = ""; foreach (var elem in names ?? new string[0]) { queryParams = string.Concat(queryParams, "names=", Uri.EscapeDataString(elem), "&"); } foreach (var elem in labels ?? new string[0]) { queryParams = string.Concat(queryParams, "labels=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } if (!string.IsNullOrEmpty(bearerToken)) { var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); } byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Enrich/replace the current session with new identifier. /// public async Task SatoriIdentifyAsync( string bearerToken, ApiIdentifyRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v1/identify"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "PUT"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Delete the caller's identity and associated data. /// public async Task SatoriDeleteIdentityAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v1/identity"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// List available live events. /// public async Task SatoriGetLiveEventsAsync( string bearerToken, IEnumerable names, IEnumerable labels, int? pastRunCount, int? futureRunCount, string startTimeSec, string endTimeSec, CancellationToken? cancellationToken) { var urlpath = "/v1/live-event"; var queryParams = ""; foreach (var elem in names ?? new string[0]) { queryParams = string.Concat(queryParams, "names=", Uri.EscapeDataString(elem), "&"); } foreach (var elem in labels ?? new string[0]) { queryParams = string.Concat(queryParams, "labels=", Uri.EscapeDataString(elem), "&"); } if (pastRunCount != null) { queryParams = string.Concat(queryParams, "past_run_count=", pastRunCount, "&"); } if (futureRunCount != null) { queryParams = string.Concat(queryParams, "future_run_count=", futureRunCount, "&"); } if (startTimeSec != null) { queryParams = string.Concat(queryParams, "start_time_sec=", Uri.EscapeDataString(startTimeSec), "&"); } if (endTimeSec != null) { queryParams = string.Concat(queryParams, "end_time_sec=", Uri.EscapeDataString(endTimeSec), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Join an 'explicit join' live event. /// public async Task SatoriJoinLiveEventAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v1/live-event/{id}/participation"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Get the list of messages for the identity. /// public async Task SatoriGetMessageListAsync( string bearerToken, int? limit, bool? forward, string cursor, IEnumerable messageIds, CancellationToken? cancellationToken) { var urlpath = "/v1/message"; var queryParams = ""; if (limit != null) { queryParams = string.Concat(queryParams, "limit=", limit, "&"); } if (forward != null) { queryParams = string.Concat(queryParams, "forward=", forward.ToString().ToLower(), "&"); } if (cursor != null) { queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); } foreach (var elem in messageIds ?? new string[0]) { queryParams = string.Concat(queryParams, "message_ids=", Uri.EscapeDataString(elem), "&"); } string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Deletes a message for an identity. /// public async Task SatoriDeleteMessageAsync( string bearerToken, string id, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } var urlpath = "/v1/message/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "DELETE"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Updates a message for an identity. /// public async Task SatoriUpdateMessageAsync( string bearerToken, string id, ApiUpdateMessageRequest body, CancellationToken? cancellationToken) { if (id == null) { throw new ArgumentException("'id' is required but was null."); } if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v1/message/{id}"; urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "PUT"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// List properties associated with this identity. /// public async Task SatoriListPropertiesAsync( string bearerToken, CancellationToken? cancellationToken) { var urlpath = "/v1/properties"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "GET"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson(); } /// /// Update identity properties. /// public async Task SatoriUpdatePropertiesAsync( string bearerToken, ApiUpdatePropertiesRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v1/properties"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "PUT"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } /// /// Publish server events for multiple distinct identities. /// public async Task SatoriServerEventAsync( string bearerToken, ApiEventRequest body, CancellationToken? cancellationToken) { if (body == null) { throw new ArgumentException("'body' is required but was null."); } var urlpath = "/v1/server-event"; var queryParams = ""; string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "POST"; var headers = new Dictionary(); var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); byte[] content = null; var jsonBody = body.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); } } } ================================================ FILE: Satori/Client.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Satori { /// public class Client : IClient { /// /// The default expired timespan used to check session lifetime. /// public static TimeSpan DefaultExpiredTimeSpan = TimeSpan.FromMinutes(5); /// public string ApiKey { get; } /// public bool AutoRefreshSession { get; } /// public RetryConfiguration GlobalRetryConfiguration { get; set; } = new RetryConfiguration( baseDelayMs: 500, jitter: RetryJitter.FullJitter, listener: null, maxRetries: 4); /// public string Host { get; } /// public ILogger Logger { get => _logger; set { _apiClient.HttpAdapter.Logger = value; _logger = value; } } /// public int Port { get; } /// public string Scheme { get; } /// public event Action ReceivedSessionUpdated; /// public int Timeout { get => _apiClient.Timeout; set => _apiClient.Timeout = value; } /// /// The default timeout of the server. /// public const int DefaultTimeout = 15; private readonly ApiClient _apiClient; private ILogger _logger; private readonly RetryInvoker _retryInvoker; public Client(string scheme, string host, int port, string apiKey) : this(scheme, host, port, apiKey, HttpRequestAdapter.WithGzip()) { } public Client(string scheme, string host, int port, string apiKey, IHttpAdapter adapter, bool autoRefreshSession = true) { Host = host; Port = port; Scheme = scheme; ApiKey = apiKey; AutoRefreshSession = autoRefreshSession; _apiClient = new ApiClient(new UriBuilder(scheme, host, port).Uri, adapter, DefaultTimeout); _retryInvoker = new RetryInvoker(adapter.TransientExceptionDelegate); Logger = NullLogger.Instance; // must set logger last. } /// public async Task AuthenticateAsync(string id, Dictionary defaultProperties = default, Dictionary customProperties = default, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { var resp = await _retryInvoker.InvokeWithRetry(() => _apiClient.SatoriAuthenticateAsync(ApiKey, string.Empty, new ApiAuthenticateRequest { Id = id, _default = defaultProperties, _custom = customProperties }, cancellationToken), new RetryHistory(id, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); return new Session(resp.Token, resp.RefreshToken); } /// public Task AuthenticateLogoutAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) => _retryInvoker.InvokeWithRetry(() => _apiClient.SatoriAuthenticateLogoutAsync(session.AuthToken, new ApiAuthenticateLogoutRequest { RefreshToken = session.RefreshToken, Token = session.AuthToken }, cancellationToken), new RetryHistory(session.AuthToken, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); /// public async Task EventAsync(ISession session, Event @event, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } var request = new ApiEventRequest { _events = new List { @event.ToApiEvent() } }; await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriEventAsync(session.AuthToken, request, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task EventsAsync(ISession session, IEnumerable events, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } var apiEventList = new List(); foreach (var evt in events) { apiEventList.Add(evt.ToApiEvent()); } var request = new ApiEventRequest { _events = apiEventList }; await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriEventAsync(session.AuthToken, request, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public Task GetAllExperimentsAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) => GetExperimentsAsync(session, null, null, cancellationToken, retryConfiguration); /// public async Task GetExperimentsAsync(ISession session, IEnumerable names, IEnumerable labels, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriGetExperimentsAsync(session.AuthToken, names, labels, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task GetFlagAsync(ISession session, string name, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { var resp = await GetFlagsAsync(session, new[] { name }, null, cancellationToken, retryConfiguration); foreach (var flag in resp.Flags) { if (flag.Name.Equals(name)) { return flag; } } throw new ArgumentException($"flag '{name}' not found."); } /// public Task GetFlagAsync(ISession session, string name, string defaultValue, CancellationToken? cancellationToken = default) { try { return GetFlagAsync(session, name, cancellationToken); } catch (ArgumentException) { return Task.FromResult(new ApiFlag { Name = name, Value = defaultValue, ConditionChanged = false }); } catch (ApiResponseException e) { if (_apiClient.HttpAdapter.TransientExceptionDelegate.Invoke(e)) { return Task.FromResult(new ApiFlag { Name = name, Value = defaultValue, ConditionChanged = false }); } throw; } } /// public async Task GetFlagDefaultAsync(string name, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { var resp = await GetFlagsDefaultAsync(new[] { name }, null, cancellationToken, retryConfiguration); if (!resp.Flags.Any()) { throw new ArgumentException($"flag '{name}' not found."); } return resp.Flags.First(); } /// public Task GetFlagDefaultAsync(string name, string defaultValue, CancellationToken? cancellationToken = default) { try { return GetFlagDefaultAsync(name, cancellationToken); } catch (ArgumentException) { return Task.FromResult(new ApiFlag { Name = name, Value = defaultValue, ConditionChanged = false }); } catch (ApiResponseException e) { if (_apiClient.HttpAdapter.TransientExceptionDelegate.Invoke(e)) { return Task.FromResult(new ApiFlag { Name = name, Value = defaultValue, ConditionChanged = false }); } throw; } } /// public async Task GetFlagsAsync(ISession session, IEnumerable names, IEnumerable labels, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.SatoriGetFlagsAsync(string.Empty, string.Empty, session.AuthToken, names, labels, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public Task GetFlagsDefaultAsync(IEnumerable names, IEnumerable labels, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { return _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriGetFlagsAsync(this.ApiKey, string.Empty, string.Empty, names, labels, cancellationToken), new RetryHistory(string.Empty, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task IdentifyAsync(ISession session, string id, Dictionary defaultProperties, Dictionary customProperties, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } var req = new ApiIdentifyRequest { Id = id, _default = defaultProperties, _custom = customProperties }; var resp = await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriIdentifyAsync(session.AuthToken, req, cancellationToken), new RetryHistory(id, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); var session2 = new Session(resp.Token, resp.RefreshToken); if (session is Session updatedSession) { // Update session object in place if we can. updatedSession.Update(resp.Token, resp.RefreshToken); return updatedSession; } return session2; } /// public async Task GetLiveEventsAsync( ISession session, IEnumerable names = null, IEnumerable labels = null, int? pastRunCount = null, int? futureRunCount = null, string startTimeSec = null, string endTimeSec = null, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriGetLiveEventsAsync(session.AuthToken, names, labels, pastRunCount, futureRunCount, startTimeSec, endTimeSec, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task JoinLiveEventAsync(ISession session, string id = null, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriJoinLiveEventAsync(session.AuthToken, id, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task ListPropertiesAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriListPropertiesAsync(session.AuthToken, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task SessionRefreshAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { var resp = await _retryInvoker.InvokeWithRetry(() => _apiClient.SatoriAuthenticateRefreshAsync(ApiKey, string.Empty, new ApiAuthenticateRefreshRequest { RefreshToken = session.RefreshToken }, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); if (session is Session updatedSession) { // Update session object in place if we can. updatedSession.Update(resp.Token, resp.RefreshToken); ReceivedSessionUpdated?.Invoke(updatedSession); return updatedSession; } var newSession = new Session(resp.Token, resp.RefreshToken); ReceivedSessionUpdated?.Invoke(newSession); return newSession; } /// public async Task UpdatePropertiesAsync(ISession session, Dictionary defaultProperties, Dictionary customProperties, bool recompute = false, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken, retryConfiguration); } ApiUpdatePropertiesRequest payload = new ApiUpdatePropertiesRequest { _default = defaultProperties, _custom = customProperties, Recompute = recompute, }; await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriUpdatePropertiesAsync(session.AuthToken, payload, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task DeleteIdentityAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriDeleteIdentityAsync(session.AuthToken, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task GetMessageListAsync(ISession session, int limit = 1, bool forward = true, string cursor = null, IEnumerable messageIds = null, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } return await _retryInvoker.InvokeWithRetry(() => _apiClient.SatoriGetMessageListAsync(session.AuthToken, limit, forward, cursor, messageIds, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task UpdateMessageAsync(ISession session, string id, string consumeTime, string readTime, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } await _retryInvoker.InvokeWithRetry(() => _apiClient.SatoriUpdateMessageAsync(session.AuthToken, id, new ApiUpdateMessageRequest() { ConsumeTime = consumeTime, ReadTime = readTime }, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task DeleteMessageAsync(ISession session, string id, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriDeleteMessageAsync(session.AuthToken, id, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } /// public async Task GetFlagOverridesAsync(ISession session, IEnumerable names = null, IEnumerable labels = null, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null) { if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) { await SessionRefreshAsync(session, cancellationToken); } return await _retryInvoker.InvokeWithRetry( () => _apiClient.SatoriGetFlagOverridesAsync(string.Empty, string.Empty, session.AuthToken, names, labels, cancellationToken), new RetryHistory(session, retryConfiguration ?? GlobalRetryConfiguration, cancellationToken)); } } } ================================================ FILE: Satori/Console/ConsoleClient.gen.cs ================================================ /* Code generated by codegen/main.go. DO NOT EDIT. */ namespace Satori.Console { using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; using TinyJson; /// /// An exception generated for HttpResponse objects don't return a success status. /// public sealed class ApiResponseException : Exception { public long StatusCode { get; } public int GrpcStatusCode { get; } public ApiResponseException(long statusCode, string content, int grpcCode) : base(content) { StatusCode = statusCode; GrpcStatusCode = grpcCode; } public ApiResponseException(string message, Exception e) : base(message, e) { StatusCode = -1L; GrpcStatusCode = -1; } public ApiResponseException(string content) : this(-1L, content, -1) { } public override string ToString() { return $"ApiResponseException(StatusCode={StatusCode}, Message='{Message}', GrpcStatusCode={GrpcStatusCode})"; } } /// /// The low level client for the Satori.Console API. /// internal class ApiClient { public readonly IHttpAdapter HttpAdapter; public int Timeout { get; set; } private readonly Uri _baseUri; public ApiClient(Uri baseUri, IHttpAdapter httpAdapter, int timeout = 10) { _baseUri = baseUri; HttpAdapter = httpAdapter; Timeout = timeout; } } } ================================================ FILE: Satori/Event.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Xml; namespace Satori { /// /// An event to be published to the server. /// public class Event { /// /// The name of the event. /// public string Name { get; } /// /// The time when the event was triggered. /// public DateTime Timestamp { get; } /// /// Optional value. /// public string Value { get; } /// /// Event metadata, if any. /// public Dictionary Metadata { get; } /// /// Optional event ID assigned by the client, used to de-duplicate in retransmission scenarios. /// If not supplied the server will assign a randomly generated unique event identifier. /// public string Id { get; } /// /// Optional identity id associated with the event. Ignored if the event is published as part of a session. /// public string IdentityId { get; } /// /// Optional session id associated with the event. Ignored if the event is published as part of a session. /// public string SessionId { get; } /// /// Optional Unix epoch session issued at associated with the event. Ignored if the event is published as part of a session. /// public string SessionIssuedAt { get; } /// /// Optional Unix epoch session expires at associated with the event. Ignored if the event is published as part of a session. /// public string SessionExpiresAt { get; } /// /// The event constructor. /// /// The /// The /// The /// The /// The /// /// /// /// public Event(string name, DateTime timestamp, string value = null, Dictionary metadata = null, string id = null, string sessionId = null, string identityId = null, string sessionIssuedAt = null, string sessionExpiresAt = null) { Name = name; Timestamp = timestamp; Value = value; Metadata = metadata; Id = id; SessionId = sessionId; IdentityId = identityId; SessionIssuedAt = sessionIssuedAt; SessionExpiresAt = sessionExpiresAt; } internal ApiEvent ToApiEvent() { return new ApiEvent() { Id = this.Id, Name = this.Name, // Protobuf requires a DateTime string formatted as per RFC 3339 Timestamp = XmlConvert.ToString(this.Timestamp, XmlDateTimeSerializationMode.Utc), Value = this.Value, _metadata = this.Metadata, IdentityId = this.IdentityId, SessionId = this.SessionId, SessionIssuedAt = this.SessionIssuedAt, SessionExpiresAt = this.SessionExpiresAt, }; } } } ================================================ FILE: Satori/GZipHttpClientHandler.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.IO.Compression; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace Satori { internal class GZipHttpClientHandler : DelegatingHandler { public GZipHttpClientHandler(HttpMessageHandler innerHandler) { InnerHandler = innerHandler; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken ct) { if ((request.Method == HttpMethod.Post || request.Method == HttpMethod.Put) && request.Content != null) { request.Content = new GZipContent(request.Content); } return base.SendAsync(request, ct); } } internal class GZipContent : HttpContent { private readonly HttpContent _content; public GZipContent(HttpContent content) { _content = content; // Must copy all pre-existing headers. foreach (var header in content.Headers) { Headers.TryAddWithoutValidation(header.Key, header.Value); } Headers.ContentEncoding.Add("gzip"); } protected override async Task SerializeToStreamAsync(System.IO.Stream stream, TransportContext context) { using (var gzip = new GZipStream(stream, CompressionMode.Compress, true)) { await _content.CopyToAsync(gzip); } } protected override bool TryComputeLength(out long length) { length = -1; return false; } } } ================================================ FILE: Satori/HttpRequestAdapter.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Satori.TinyJson; namespace Satori { /// /// HTTP Request adapter which uses the .NET HttpClient to send requests. /// /// /// Accept header is always set as 'application/json'. /// public class HttpRequestAdapter : IHttpAdapter { /// public ILogger Logger { get; set; } public TransientExceptionDelegate TransientExceptionDelegate => IsTransientException; private readonly HttpClient _httpClient; public HttpRequestAdapter(HttpClient httpClient) { _httpClient = httpClient; _httpClient.Timeout = TimeSpan.FromSeconds(80); // Provide a global request timeout as a failsafe. } /// public async Task SendAsync(string method, Uri uri, IDictionary headers, byte[] body, int timeout, CancellationToken? userCancelToken) { var request = new HttpRequestMessage { RequestUri = uri, Method = new HttpMethod(method) }; request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); foreach (var kv in headers) { request.Headers.TryAddWithoutValidation(kv.Key, kv.Value); } if (body != null) { request.Content = new ByteArrayContent(body); Logger?.InfoFormat("Send: method='{0}', uri='{1}', body='{2}'", method, uri, System.Text.Encoding.UTF8.GetString(body)); } else { Logger?.InfoFormat("Send: method='{0}', uri='{1}'", method, uri); } using var ctsTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsTimeout.Token, userCancelToken ?? CancellationToken.None); try { using var response = await _httpClient.SendAsync(request, cts.Token).ConfigureAwait(false); var contents = await response.Content.ReadAsStringAsync(); if ((int)response.StatusCode >= 500) { Logger?.ErrorFormat("Received: status={0}, contents='{1}'", response.StatusCode, contents); // TODO think of best way to map HTTP code to GRPC code since we can't rely // on server to process it. Manually adding the mapping to SDK seems brittle. throw new ApiResponseException((int)response.StatusCode, contents, -1); } if (response.IsSuccessStatusCode) { Logger?.InfoFormat("Received: status={0}, contents='{1}'", response.StatusCode, contents); return contents; } Logger?.ErrorFormat("Received: status={0}, contents='{1}'", response.StatusCode, contents); var decoded = contents.FromJson>(); var message = decoded.TryGetValue("message", out var value1) ? value1.ToString() : string.Empty; var grpcCode = decoded.TryGetValue("code", out var value2) ? (int)value2 : -1; var exception = new ApiResponseException((int)response.StatusCode, message, grpcCode); if (decoded.TryGetValue("error", out var value)) { HttpAdapterUtil.CopyResponseError(this, value, exception); } throw exception; } catch (TaskCanceledException e) when (ctsTimeout.IsCancellationRequested) { Logger?.ErrorFormat("Request timed out: method='{0}', uri='{1}'", method, uri); throw new TimeoutException($"The request timed out after {timeout} seconds.", e); } catch (Exception exception) when (!(exception is ApiResponseException)) { Logger?.ErrorFormat("Request failed: method='{0}', uri='{1}', exception='{2}'", method, uri, exception); throw; } } /// /// A new HTTP adapter with configuration for gzip support in the underlying HTTP client. /// /// /// NOTE Decompression does not work with Mono AOT on Android. /// /// If automatic decompression should be enabled with the HTTP adapter. /// If automatic compression should be enabled with the HTTP adapter. /// A new HTTP adapter. public static IHttpAdapter WithGzip(bool decompression = false, bool compression = false) { var handler = new HttpClientHandler(); if (handler.SupportsAutomaticDecompression && decompression) { handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; } handler.AllowAutoRedirect = true; var client = new HttpClient(compression ? (HttpMessageHandler)new GZipHttpClientHandler(handler) : handler); return new HttpRequestAdapter(client); } public static bool IsTransientException(Exception e) { if (e is ApiResponseException apiException) { switch (apiException.StatusCode) { case 500 : // Internal Server Error often (but not always) indicates a transient issue in Nakama, e.g., DB connectivity. case 502 : // LB returns this to client if server sends corrupt/invalid data to LB, which may be a transient issue. case 503 : // LB returns this to client if LB determines or is told that server is unable to handle forwarded from LB, which may be a transient issue. case 504 : // LB returns this to client if LB cannot communicate with server, which may be a temporary issue. return true; } } return false; } } } ================================================ FILE: Satori/IClient.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Satori { /// /// A client for the API in Satori server. /// public interface IClient { /// /// The key used to authenticate with the server without a session. /// string ApiKey { get; } /// /// True if the session should be refreshed with an active refresh token. /// bool AutoRefreshSession { get; } /// /// The global retry configuration. See . /// RetryConfiguration GlobalRetryConfiguration { get; set; } /// /// The host address of the server. /// string Host { get; } /// /// The logger to use with the client. /// ILogger Logger { get; set; } /// /// The port number of the server. /// int Port { get; } /// /// The protocol scheme used to connect with the server. Must be either "http" or "https". /// string Scheme { get; } /// /// Set the timeout in seconds on requests sent to the server. /// int Timeout { get; set; } /// /// Received a new session after the current one has expired. /// /// /// This event will only be sent when SessionRefreshAsync is called which also happens automatically if /// AutoRefreshSession is enabled. /// /// /// event Action ReceivedSessionUpdated; /// /// Authenticate against the server. /// /// An optional user id. /// Optional default properties to update with this call. If not set, properties are left as they are on the server. /// Optional custom properties to update with this call. If not set, properties are left as they are on the server. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to a user session. public Task AuthenticateAsync(string id, Dictionary defaultProperties = default, Dictionary customProperties = default, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. /// /// The session of the user. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which represents the asynchronous operation. public Task AuthenticateLogoutAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Send an event for this session. /// /// The session of the user. /// The event to send. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task object. public Task EventAsync(ISession session, Event @event, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Send a batch of events for this session. /// /// The session of the user. /// The batch of events which will be sent. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task object. public Task EventsAsync(ISession session, IEnumerable events, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Get all experiments data. /// /// The session of the user. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to all experiments that this identity is involved with. public Task GetAllExperimentsAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Get specific experiments data. /// /// The session of the user. /// Experiment names; if empty string, all experiments are returned based on the remaining filters. /// Label names that must be defined for each Experiment; if empty string, all experiments are returned based on the remaining filters. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to all experiments that this identity is involved with. public Task GetExperimentsAsync(ISession session, IEnumerable names, IEnumerable labels, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Get a single flag for this identity. /// /// /// Unlike GetFlags(ISession,string,CancellationToken) this method will return the default value /// specified and will not raise an exception if the network is unavailable. /// /// The session of the user. /// The name of the flag. /// The default value if the server is unreachable. /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a single feature flag. public Task GetFlagAsync(ISession session, string name, string defaultValue, CancellationToken? cancellationToken = default); /// /// Get a single default flag for this identity. /// /// The name of the flag. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to a single default feature flag. public Task GetFlagDefaultAsync(string name, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Get a single default flag for this identity. /// /// /// Unlike GetFlagDefaultAsync(string,string,CancellationToken) this method will return the default /// value specified and will not raise an exception if the network is unreachable. /// /// The name of the flag. /// The default value if the server is unreachable. /// The that can be used to cancel the request while mid-flight. /// A task which resolves to a single default feature flag. public Task GetFlagDefaultAsync(string name, string defaultValue, CancellationToken? cancellationToken = default); /// /// List all available flags for this identity. /// /// The session of the user. /// Flag names, if empty string all flags are returned. /// Label names that must be defined for each Flag; if empty string, all flags are returned based on the remaining filters. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to all flags available to this identity. public Task GetFlagsAsync(ISession session, IEnumerable names, IEnumerable labels, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// List all available default flags. /// /// Flag names, if empty string all flags are returned. /// Label names that must be defined for each Flag; if empty string, all flags are returned based on the remaining filters. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to all available default flags. public Task GetFlagsDefaultAsync(IEnumerable names, IEnumerable labels, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// List available live events. /// /// The session of the user. /// Live event names, if null or empty, all live events are returned. /// Label names that must be defined for each Live Event; if empty string, all live events are returned based on the remaining filters. /// The maximum number of past event runs to return for each live event. /// The maximum number of future event runs to return for each live event. /// Start time of the time window filter to apply. /// End time of the time window filter to apply. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to a list of live events. public Task GetLiveEventsAsync( ISession session, IEnumerable names = null, IEnumerable labels = null, int? pastRunCount = null, int? futureRunCount = null, string startTimeSec = null, string endTimeSec = null, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null ); /// /// Join an 'explicit join' live event. /// /// The session of the user. /// Live event id to join. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See public Task JoinLiveEventAsync(ISession session, string id = null, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Identify a session with a new ID. /// /// The session of the user. /// Identity ID to enrich the current session and return a new session. The old session will /// no longer be usable. Must be between eight and 128 characters (inclusive). Must be an alphanumeric string /// with only underscores and hyphens allowed. /// The default properties. /// The custom event properties. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to the new session for the user. public Task IdentifyAsync(ISession session, string id, Dictionary defaultProperties, Dictionary customProperties, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// List properties associated with this identity. /// /// The session of the user. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to a list of live events. public Task ListPropertiesAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Refresh a user's session using a refresh token retrieved from a previous authentication request. /// /// The session of the user. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task which resolves to a user session. public Task SessionRefreshAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Update properties associated with this identity. /// /// The session of the user. /// The default properties to update. /// The custom properties to update. /// Whether or not to recompute the user's audience membership immediately after property update. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task object. public Task UpdatePropertiesAsync(ISession session, Dictionary defaultProperties, Dictionary customProperties, bool recompute, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Delete the caller's identity and associated data. /// /// The session of the user. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task object. public Task DeleteIdentityAsync(ISession session, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Get all the messages for an identity. /// /// The session of the user. /// Max number of messages to return. Between 1 and 100. /// True if listing should be older messages to newer, false if reverse. /// A pagination cursor, if any. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task object which resolves to a list of messages. public Task GetMessageListAsync(ISession session, int limit = 1, bool forward = true, string cursor = null, IEnumerable messageIds = null, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Update the status of a message. /// /// The session of the user. /// The message's unique identifier. /// The time the message was consumed by the identity. /// The time the message was read at the client. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task object. public Task UpdateMessageAsync(ISession session, string id, string consumeTime, string readTime, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Delete a scheduled message. /// /// The session of the user. /// The identifier of the message. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task object. public Task DeleteMessageAsync(ISession session, string id, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); /// /// Get all available flags and their value overrides for this identity. /// /// The session of the user. /// Live event names, if null or empty, all live events are returned. /// Live event labels, if null or empty, all live events are returned. /// The that can be used to cancel the request while mid-flight. /// The retry configuration. See /// A task object which resolves to a list all available flags and their value overrides for this identity. public Task GetFlagOverridesAsync(ISession session, IEnumerable names = null, IEnumerable labels = null, CancellationToken? cancellationToken = default, RetryConfiguration retryConfiguration = null); } } ================================================ FILE: Satori/IHttpAdapter.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Satori { /// /// An adapter which implements the HTTP protocol. /// public interface IHttpAdapter { /// /// A delegate used to determine whether or not an error from the server is due to a temporary bad state on the /// server (i.e., is 'transient'). /// TransientExceptionDelegate TransientExceptionDelegate { get; } /// /// The logger to use with the adapter. /// ILogger Logger { get; set; } /// /// Send a HTTP request. /// /// HTTP method to use for this request. /// The fully qualified URI to use. /// Request headers to set. /// Request content body to set. /// Request timeout. /// A user-generated token that can be used to cancel the request. /// A task which resolves to the contents of the response. Task SendAsync(string method, Uri uri, IDictionary headers, byte[] body, int timeoutSec = 3, CancellationToken? userCancelToken = null); } } ================================================ FILE: Satori/IHttpAdapterUtil.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; namespace Satori { /// /// Utility methods for the interface. /// NOTE: DO NOT USE EXTENSION METHODS as Unity cannot cross-compile /// them properly to WebGL. /// public static class HttpAdapterUtil { /// /// Performs an in-place copy of data from Nakama's error response into /// the data dictionary of an . /// /// The adapter receiving the error response. /// The decoded error field from the server response. /// The exception whose data dictionary is being written to. public static void CopyResponseError(IHttpAdapter adapter, object err, ApiResponseException e) { var errString = err as string; var errDict = err as Dictionary; if (errString != null) { e.Data["error"] = err; } else if (errDict != null) { foreach (KeyValuePair keyVal in errDict) { e.Data[keyVal.Key] = keyVal.Value; } } } } } ================================================ FILE: Satori/ILogger.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Satori { /// /// A simple logger to write log messages to an output sink. /// public interface ILogger { /// /// Logs a formatted string with the DEBUG level. /// /// A string with zero or more format items. /// An object array with zero or more objects to format. void DebugFormat(string format, params object[] args); /// /// Logs a formatted string with the ERROR level. /// /// A string with zero or more format items. /// An object array with zero or more objects to format. void ErrorFormat(string format, params object[] args); /// /// Logs a formatted string with the INFO level. /// /// A string with zero or more format items. /// An object array with zero or more objects to format. void InfoFormat(string format, params object[] args); /// /// Logs a formatted string with the WARN level. /// /// A string with zero or more format items. /// An object array with zero or more objects to format. void WarnFormat(string format, params object[] args); } } ================================================ FILE: Satori/ISession.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; namespace Satori { /// /// A session authenticated for a user with Satori server. /// public interface ISession { /// /// The authorization token used to construct this session. /// string AuthToken { get; } /// /// The UNIX timestamp when this session was created. /// long CreateTime { get; } /// /// The UNIX timestamp when this session will expire. /// long ExpireTime { get; } /// /// If the session has expired. /// bool IsExpired { get; } /// /// If the refresh token has expired. /// bool IsRefreshExpired { get; } /// /// The UNIX timestamp when the refresh token will expire. /// long RefreshExpireTime { get; } /// /// Refresh token that can be used for session token renewal. /// string RefreshToken { get; } /// /// The ID of the user who owns this session. /// string IdentityId { get; } /// /// Check the session has expired against the offset time. /// /// The datetime to compare against this session. /// If the session has expired. bool HasExpired(DateTime offset); /// /// Check if the refresh token has expired against the offset time. /// /// The datetime to compare against this refresh token. /// If refresh token has expired. bool HasRefreshExpired(DateTime offset); } } ================================================ FILE: Satori/NullLogger.cs ================================================ // Copyright 2025 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Satori { /// /// A logger which writes to nowhere. /// internal class NullLogger : ILogger { public static readonly ILogger Instance = new NullLogger(); private NullLogger() { } /// public void DebugFormat(string format, params object[] args) { } /// public void ErrorFormat(string format, params object[] args) { } /// public void InfoFormat(string format, params object[] args) { } /// public void WarnFormat(string format, params object[] args) { } } } ================================================ FILE: Satori/PreserveAttribute.cs ================================================ /* * Copyright 2022 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Satori { /// /// A custom attribute recognized by Unity3D. When added to a class member, it prevents /// the Unity linker from stripping the code it is associated with. This is used in addition /// to the link.xml file because the Unity Package Manager does not recognize link.xml files /// inside Unity packages. /// https://docs.unity3d.com/2018.3/Documentation/Manual/ManagedCodeStripping.html /// internal class PreserveAttribute : System.Attribute { } } ================================================ FILE: Satori/Retry.cs ================================================ /* * Copyright 2024 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Satori { /// /// Represents a single retry attempt. /// public class Retry { /// /// The delay (milliseconds) in the request retry attributable to the exponential backoff algorithm. /// public int ExponentialBackoff { get; } /// /// The delay (milliseconds) in the request retry attributable to the jitter algorithm. /// public int JitterBackoff { get; } internal Retry(int expoBackoff, int jitterBackoff) { ExponentialBackoff = expoBackoff; JitterBackoff = jitterBackoff; } } } ================================================ FILE: Satori/RetryConfiguration.cs ================================================ /* * Copyright 2024 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Satori { /// /// A configuration for controlling retriable requests. /// /// /// Retry configurations can be assigned to the on a request-by-request basis via /// the see parameter. /// /// Retry configurations can also be assigned on a global basis using . /// Configurations passed via the see parameter take precedence over the global configuration. /// public class RetryConfiguration { /// /// The base delay (milliseconds) used to calculate the time before making another request attempt. /// This base will be raised to N, where N is the number of retry attempts. /// public int BaseDelayMs { get; } /// /// The jitter algorithm used to apply randomness to the retry delay. Defaults to /// public Jitter Jitter { get; } /// /// The maximum number of attempts to make before cancelling the request task. /// public int MaxAttempts { get; } /// /// A callback that is invoked before a new retry attempt is made. /// public RetryListener RetryListener { get; } /// /// Create a new retry configuration. /// /// The base delay (milliseconds) used to calculate the time before making another request attempt. /// The maximum number of attempts to make before cancelling the request task. public RetryConfiguration(int baseDelayMs, int maxRetries) : this(baseDelayMs, maxRetries, null, RetryJitter.FullJitter) {} /// /// Create a new retry configuration. /// /// The base delay (milliseconds) used to calculate the time before making another request attempt. /// The maximum number of attempts to make before cancelling the request task. /// A callback that is invoked before a new retry attempt is made. public RetryConfiguration(int baseDelayMs, int maxRetries, RetryListener listener) : this(baseDelayMs, maxRetries, listener, RetryJitter.FullJitter) {} /// /// Create a new retry configuration. /// /// The base delay (milliseconds) used to calculate the time before making another request attempt. /// The maximum number of attempts to make before cancelling the request task. /// A callback that is invoked before a new retry attempt is made. /// The jitter algorithm used to apply randomness to the retry delay. public RetryConfiguration(int baseDelayMs, int maxRetries, RetryListener listener, Jitter jitter) { BaseDelayMs = baseDelayMs; RetryListener = listener; MaxAttempts = maxRetries; Jitter = jitter; } } } ================================================ FILE: Satori/RetryHistory.cs ================================================ /* * Copyright 2024 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Threading; namespace Satori { internal class RetryHistory { public RetryConfiguration Configuration { get; } public List Retries { get; } public CancellationToken? UserCancelToken { get; } public Random Random { get; } public RetryHistory(ISession session, RetryConfiguration configuration, CancellationToken? userCancelToken) : this(session.AuthToken, configuration, userCancelToken) { } public RetryHistory(string jitterHashKey, RetryConfiguration configuration, CancellationToken? userCancelToken) { Configuration = configuration; Retries = new List(); UserCancelToken = userCancelToken; Random = new Random(jitterHashKey.GetHashCode()); } } } ================================================ FILE: Satori/RetryInvoker.cs ================================================ /* * Copyright 2024 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Threading.Tasks; namespace Satori { /// /// Invokes requests with retry and exponential backoff. /// internal class RetryInvoker { private readonly TransientExceptionDelegate _del; public RetryInvoker(TransientExceptionDelegate del) { if (del == null) { throw new ArgumentException("Cannot initialize retry invoker with a null transient exception delegate."); } _del = del; } public async Task InvokeWithRetry(Func> request, RetryHistory history) { try { return await request(); } catch (Exception e) { if (history.Configuration != null && _del(e)) { await Backoff(history, e); return await InvokeWithRetry(request, history); } else { throw; } } } public async Task InvokeWithRetry(Func request, RetryHistory history) { try { await request(); } catch (Exception e) { if (history.Configuration != null && _del(e)) { await Backoff(history, e); await InvokeWithRetry(request, history); } else { throw; } } } private Retry CreateNewRetry(RetryHistory history) { int expoBackoff = System.Convert.ToInt32(Math.Pow(2, history.Retries.Count)) * history.Configuration.BaseDelayMs; int jitteredBackoff = history.Configuration.Jitter(history.Retries, expoBackoff, history.Random); return new Retry(expoBackoff, jitteredBackoff); } private Task Backoff(RetryHistory history, Exception e) { if (history.Retries.Count >= history.Configuration.MaxAttempts) { throw new TaskCanceledException("Exceeded max retry attempts.", e); } Retry newRetry = CreateNewRetry(history); history.Retries.Add(newRetry); history.Configuration.RetryListener?.Invoke(history.Retries.Count, newRetry); if (history.UserCancelToken.HasValue) { return Task.Delay(newRetry.JitterBackoff, history.UserCancelToken.Value); } else { return Task.Delay(newRetry.JitterBackoff); } } } } ================================================ FILE: Satori/RetryJitter.cs ================================================ /* * Copyright 2024 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; namespace Satori { /// /// The Jitter algorithm is responsible for introducing randomness to a delay before a retry. /// /// Information about previous retry attempts. /// A delay (milliseconds) between the last failed attempt in the retry history /// and the next upcoming attempt. /// A object that has been seeded by . /// A new delay (milliseconds) between the last failed attempt in the retry history and the next upcoming attempt. public delegate int Jitter(IList retryHistory, int retryDelay, Random random); /// /// A collection of algorithms. /// public static class RetryJitter { /// /// FullJitter is a Jitter algorithm that selects a random point between now and the next retry time. /// public static int FullJitter(IList retries, int retryDelay, Random random) { return System.Convert.ToInt32(retryDelay * random.NextDouble()); } } } ================================================ FILE: Satori/RetryListener.cs ================================================ /* * Copyright 2024 Heroic Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace Satori { /// /// Listens to retry events for a particular request. /// /// The number of retries made so far, including this retry. /// An holding inromation about the retry attempt. public delegate void RetryListener(int numRetry, Retry retry); } ================================================ FILE: Satori/Satori.csproj ================================================ net46;netstandard2.1 8 true 3.0.0.0 3.0.0.0 3.0.0-dev README.md $([System.String]::new('$(GitTag)').Substring(1)) Satori Authors & contributors Heroic Labs Satori is a LiveOps server for games. Run activities on the Event Calendar and optimize player experiences with Audiences, Feature Flags, and Experiments. SatoriClient Apache-2.0 gameserver;client-library;satori;liveops https://github.com/heroiclabs/nakama-dotnet ================================================ FILE: Satori/Session.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using Satori.TinyJson; namespace Satori { /// public class Session : ISession { public static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// public string AuthToken { get; private set; } /// public long CreateTime { get; private set; } /// public long ExpireTime { get; private set; } /// public bool IsExpired => HasExpired(DateTime.UtcNow); /// public bool IsRefreshExpired => HasRefreshExpired(DateTime.UtcNow); /// public long RefreshExpireTime { get; private set; } /// public string RefreshToken { get; private set; } /// public string IdentityId { get; private set; } /// public bool HasExpired(DateTime offset) { var expireDateTime = Epoch + TimeSpan.FromSeconds(ExpireTime); return offset > expireDateTime; } /// public bool HasRefreshExpired(DateTime offset) { var expireDateTime = Epoch + TimeSpan.FromSeconds(RefreshExpireTime); return offset > expireDateTime; } public override string ToString() { return $"Session(AuthToken='{AuthToken}', ExpireTime={ExpireTime}, RefreshToken={RefreshToken}, RefreshExpireTime={RefreshExpireTime}, UserId='{IdentityId}')"; } internal Session(string authToken, string refreshToken) { RefreshExpireTime = 0L; Update(authToken, refreshToken); } /// /// Update the current session token with a new authorization token and refresh token. /// /// The authorization token to update into the session. /// The refresh token to update into the session. public void Update(string authToken, string refreshToken) { AuthToken = authToken; RefreshToken = refreshToken; var json = JwtUnpack(authToken); var decoded = json.FromJson>(); ExpireTime = Convert.ToInt64(decoded["exp"]); IdentityId = decoded["iid"].ToString(); if (decoded.TryGetValue("iat", out var value)) { CreateTime = Convert.ToInt64(value); } // Check in case clients have not updated to use refresh tokens yet. if (!string.IsNullOrEmpty(refreshToken)) { var json2 = JwtUnpack(refreshToken); var decoded2 = json2.FromJson>(); RefreshExpireTime = Convert.ToInt64(decoded2["exp"]); } } /// /// Restore a session from the auth token. /// /// /// A null or empty authentication token will return null. /// /// The authorization token to restore as a session. /// The refresh token for the session. /// A session. public static ISession Restore(string authToken, string refreshToken = null) { return string.IsNullOrEmpty(authToken) ? null : new Session(authToken, refreshToken); } private static string JwtUnpack(string jwt) { // Hack decode JSON payload from JWT. var payload = jwt.Split('.')[1]; var padLength = Math.Ceiling(payload.Length / 4.0) * 4; payload = payload.PadRight(Convert.ToInt32(padLength), '=').Replace('-', '+').Replace('_', '/'); return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payload)); } } } ================================================ FILE: Satori/TinyJson/JsonParser.cs ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Alex Parker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; namespace Satori.TinyJson { // Really simple JSON parser in ~300 lines // - Attempts to parse JSON files with minimal GC allocation // - Nice and simple "[1,2,3]".FromJson>() API // - Classes and structs can be parsed too! // class Foo { public int Value; } // "{\"Value\":10}".FromJson() // - Can parse JSON without type information into Dictionary and List e.g. // "[1,2,3]".FromJson().GetType() == typeof(List) // "{\"Value\":10}".FromJson().GetType() == typeof(Dictionary) // - No JIT Emit support to support AOT compilation on iOS // - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead. // - Only public fields and property setters on classes/structs will be written to // // Limitations: // - No JIT Emit support to parse structures quickly // - Limited to parsing <2GB JSON files (due to int.MaxValue) // - Parsing of abstract classes or interfaces is NOT supported and will throw an exception. public static class JsonParser { [ThreadStatic] private static Stack> _splitArrayPool; [ThreadStatic] private static StringBuilder _stringBuilder; [ThreadStatic] private static Dictionary> _fieldInfoCache; [ThreadStatic] private static Dictionary> _propertyInfoCache; public static T FromJson(this string json) { // Initialize, if needed, the ThreadStatic variables if (_propertyInfoCache == null) { _propertyInfoCache = new Dictionary>(); } if (_fieldInfoCache == null) { _fieldInfoCache = new Dictionary>(); } if (_stringBuilder == null) { _stringBuilder = new StringBuilder(); } if (_splitArrayPool == null) { _splitArrayPool = new Stack>(); } // Remove all whitespace not within strings to make parsing simpler _stringBuilder.Length = 0; for (var i = 0; i < json.Length; i++) { var c = json[i]; if (c == '"') { i = AppendUntilStringEnd(true, i, json); continue; } if (char.IsWhiteSpace(c)) continue; _stringBuilder.Append(c); } //Parse the thing! return (T) ParseValue(typeof(T), _stringBuilder.ToString()); } private static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) { _stringBuilder.Append(json[startIdx]); for (var i = startIdx + 1; i < json.Length; i++) { if (json[i] == '\\') { if (appendEscapeCharacter) _stringBuilder.Append(json[i]); _stringBuilder.Append(json[i + 1]); i++; //Skip next character as it is escaped } else if (json[i] == '"') { _stringBuilder.Append(json[i]); return i; } else { _stringBuilder.Append(json[i]); } } return json.Length - 1; } //Splits { :, : } and [ , ] into a list of strings private static List Split(string json) { var splitArray = _splitArrayPool.Count > 0 ? _splitArrayPool.Pop() : new List(); splitArray.Clear(); if (json.Length == 2) return splitArray; var parseDepth = 0; _stringBuilder.Length = 0; for (var i = 1; i < json.Length - 1; i++) { if (json[i] == '[' || json[i] == '{') { parseDepth++; } else if (json[i] == ']' || json[i] == '}') { parseDepth--; } else if (json[i] == '"') { i = AppendUntilStringEnd(true, i, json); continue; } else if (json[i] == ',' || json[i] == ':') { if (parseDepth == 0) { splitArray.Add(_stringBuilder.ToString()); _stringBuilder.Length = 0; continue; } } _stringBuilder.Append(json[i]); } splitArray.Add(_stringBuilder.ToString()); return splitArray; } private static object ParseValue(Type type, string json) { if (type == typeof(string)) { // Return the raw value if it is unquoted (e.g. a number) var validQuotes = new[] {'"', '\''}; if (json.Length > 0 && !validQuotes.Contains(json[0]) && !validQuotes.Contains(json[json.Length-1])) { return json; } if (json.Length <= 2) return string.Empty; var parseStringBuilder = new StringBuilder(json.Length); for (var i = 1; i < json.Length - 1; ++i) { if (json[i] == '\\' && i + 1 < json.Length - 1) { var j = "\"\\nrtbf/".IndexOf(json[i + 1]); if (j >= 0) { parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]); ++i; continue; } if (json[i + 1] == 'u' && i + 5 < json.Length - 1) { uint c; if (uint.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c)) { parseStringBuilder.Append((char) c); i += 5; continue; } } } parseStringBuilder.Append(json[i]); } return parseStringBuilder.ToString(); } if (type.IsPrimitive) { var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture); return result; } if (type == typeof(decimal)) { decimal result; decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); return result; } if (json == "null") { return null; } if (type.IsEnum) { if (json[0] == '"') json = json.Substring(1, json.Length - 2); try { return Enum.Parse(type, json, false); } catch { return 0; } } if (type.IsArray) { var arrayType = type.GetElementType(); if (json[0] != '[' || json[json.Length - 1] != ']') return null; var elems = Split(json); var newArray = Array.CreateInstance(arrayType, elems.Count); for (var i = 0; i < elems.Count; i++) newArray.SetValue(ParseValue(arrayType, elems[i]), i); _splitArrayPool.Push(elems); return newArray; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { var listType = type.GetGenericArguments()[0]; if (json[0] != '[' || json[json.Length - 1] != ']') return null; var elems = Split(json); var list = (IList) type.GetConstructor(new Type[] {typeof(int)}).Invoke(new object[] {elems.Count}); foreach (var t in elems) list.Add(ParseValue(listType, t)); _splitArrayPool.Push(elems); return list; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { Type keyType, valueType; { var args = type.GetGenericArguments(); keyType = args[0]; valueType = args[1]; } // Refuse to parse dictionary keys that aren't of type string if (keyType != typeof(string)) return null; // Must be a valid dictionary element if (json[0] != '{' || json[json.Length - 1] != '}') return null; // The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON var elems = Split(json); if (elems.Count % 2 != 0) return null; var dictionary = (IDictionary) type.GetConstructor(new Type[] {typeof(int)}) .Invoke(new object[] {elems.Count / 2}); for (var i = 0; i < elems.Count; i += 2) { if (elems[i].Length <= 2) continue; var keyValue = elems[i].Substring(1, elems[i].Length - 2); var val = ParseValue(valueType, elems[i + 1]); dictionary.Add(keyValue, val); } return dictionary; } if (type == typeof(object)) { return ParseAnonymousValue(json); } if (json[0] == '{' && json[json.Length - 1] == '}') { return ParseObject(type, json); } return null; } private static object ParseAnonymousValue(string json) { if (json.Length == 0) return null; if (json[0] == '{' && json[json.Length - 1] == '}') { var elems = Split(json); if (elems.Count % 2 != 0) return null; var dict = new Dictionary(elems.Count / 2); for (var i = 0; i < elems.Count; i += 2) dict.Add(elems[i].Substring(1, elems[i].Length - 2), ParseAnonymousValue(elems[i + 1])); return dict; } if (json[0] == '[' && json[json.Length - 1] == ']') { var items = Split(json); var finalList = new List(items.Count); foreach (var t in items) finalList.Add(ParseAnonymousValue(t)); return finalList; } if (json[0] == '"' && json[json.Length - 1] == '"') { var str = json.Substring(1, json.Length - 2); return str.Replace("\\", string.Empty); } if (char.IsDigit(json[0]) || json[0] == '-') { if (json.Contains(".")) { double result; double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); return result; } else { int result; int.TryParse(json, out result); return result; } } if (json == "true") return true; if (json == "false") return false; // handles json == "null" as well as invalid JSON return null; } private static Dictionary CreateMemberNameDictionary(IEnumerable members) where T : MemberInfo { // NOTE The StringComparer is disabled intentionally because of how our generated code. // var nameToMember = new Dictionary(StringComparer.OrdinalIgnoreCase); var nameToMember = new Dictionary(); foreach (var member in members) { if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue; var name = member.Name; if (member.IsDefined(typeof(DataMemberAttribute), true)) { var dataMemberAttribute = (DataMemberAttribute) Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) name = dataMemberAttribute.Name; } nameToMember.Add(name, member); } return nameToMember; } private static object ParseObject(Type type, string json) { var instance = FormatterServices.GetUninitializedObject(type); // The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON var elems = Split(json); if (elems.Count % 2 != 0) return instance; Dictionary nameToField; Dictionary nameToProperty; if (!_fieldInfoCache.TryGetValue(type, out nameToField)) { nameToField = CreateMemberNameDictionary( type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); _fieldInfoCache.Add(type, nameToField); } if (!_propertyInfoCache.TryGetValue(type, out nameToProperty)) { nameToProperty = CreateMemberNameDictionary( type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); _propertyInfoCache.Add(type, nameToProperty); } for (var i = 0; i < elems.Count; i += 2) { if (elems[i].Length <= 2) continue; var key = elems[i].Substring(1, elems[i].Length - 2); var value = elems[i + 1]; FieldInfo fieldInfo; PropertyInfo propertyInfo; if (nameToField.TryGetValue(key, out fieldInfo)) fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value)); else if (nameToProperty.TryGetValue(key, out propertyInfo)) propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null); } return instance; } } } ================================================ FILE: Satori/TinyJson/JsonWriter.cs ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Alex Parker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Runtime.Serialization; using System.Text; namespace Satori.TinyJson { // Really simple JSON writer // - Outputs JSON structures from an object // - Really simple API (new List { 1, 2, 3 }).ToJson() == "[1,2,3]" // - Will only output public fields and property getters on objects public static class JsonWriter { public static string ToJson(this object item) { var stringBuilder = new StringBuilder(); AppendValue(stringBuilder, item); return stringBuilder.ToString(); } private static void AppendValue(StringBuilder stringBuilder, object item) { if (item == null) { stringBuilder.Append("null"); return; } var type = item.GetType(); if (type == typeof(string) || type == typeof(char)) { stringBuilder.Append('"'); var str = (string) item; foreach (var t in str) if (t < ' ' || t == '"' || t == '\\') { stringBuilder.Append('\\'); var j = "\"\\\n\r\t\b\f".IndexOf(t); if (j >= 0) stringBuilder.Append("\"\\nrtbf"[j]); else stringBuilder.AppendFormat("u{0:X4}", (uint) t); } else stringBuilder.Append(t); stringBuilder.Append('"'); } else if (type == typeof(byte) || type == typeof(sbyte)) { stringBuilder.Append(item); } else if (type == typeof(short) || type == typeof(ushort)) { stringBuilder.Append(item); } else if (type == typeof(int) || type == typeof(uint)) { stringBuilder.Append(item); } else if (type == typeof(long) || type == typeof(ulong)) { stringBuilder.Append(item); } else if (type == typeof(float)) { stringBuilder.Append(((float) item).ToString(System.Globalization.CultureInfo.InvariantCulture)); } else if (type == typeof(double)) { stringBuilder.Append(((double) item).ToString(System.Globalization.CultureInfo.InvariantCulture)); } else if (type == typeof (decimal)) { stringBuilder.Append (((decimal) item).ToString (System.Globalization.CultureInfo.InvariantCulture)); } else if (type == typeof(bool)) { stringBuilder.Append((bool) item ? "true" : "false"); } else if (type.IsEnum) { stringBuilder.Append('"'); stringBuilder.Append(item); stringBuilder.Append('"'); } else if (item is IList) { stringBuilder.Append('['); var isFirst = true; var list = (IList) item; foreach (var t in list) { if (isFirst) isFirst = false; else stringBuilder.Append(','); AppendValue(stringBuilder, t); } stringBuilder.Append(']'); } else if (item is IDictionary dict) { var keyType = type.GetGenericArguments()[0]; //Refuse to output dictionary keys that aren't of type string if (keyType != typeof(string)) { stringBuilder.Append("{}"); return; } stringBuilder.Append('{'); var isFirst = true; foreach (var key in dict.Keys) { if (isFirst) isFirst = false; else stringBuilder.Append(','); stringBuilder.Append('\"'); stringBuilder.Append((string) key); stringBuilder.Append("\":"); AppendValue(stringBuilder, dict[key]); } stringBuilder.Append('}'); } else { stringBuilder.Append('{'); var isFirst = true; var fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); foreach (var t in fieldInfos) { if (t.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue; var value = t.GetValue(item); if (value == null) continue; if (isFirst) isFirst = false; else stringBuilder.Append(','); stringBuilder.Append('\"'); stringBuilder.Append(GetMemberName(t)); stringBuilder.Append("\":"); AppendValue(stringBuilder, value); } var propertyInfo = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); foreach (var t in propertyInfo) { if (!t.CanRead || t.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue; var value = t.GetValue(item, null); if (value == null) continue; if (isFirst) isFirst = false; else stringBuilder.Append(','); stringBuilder.Append('\"'); stringBuilder.Append(GetMemberName(t)); stringBuilder.Append("\":"); AppendValue(stringBuilder, value); } stringBuilder.Append('}'); } } private static string GetMemberName(MemberInfo member) { if (!member.IsDefined(typeof(DataMemberAttribute), true)) return member.Name; var dataMemberAttribute = (DataMemberAttribute) Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); return !string.IsNullOrEmpty(dataMemberAttribute.Name) ? dataMemberAttribute.Name : member.Name; } } } ================================================ FILE: Satori/TinyJson/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018 Alex Parker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Satori/TransientExceptionDelegate.cs ================================================ // Copyright 2022 The Satori Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; namespace Satori { /// /// A delegate used to determine whether or not a network exception is /// due to a temporary bad state on the server. For example, timeouts can be transient in cases where /// the server is experiencing temporarily high load. /// public delegate bool TransientExceptionDelegate(Exception e); } ================================================ FILE: Satori.Tests/ClientIdentifyTest.cs ================================================ // Copyright 2022 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace Satori.Tests { public class ClientIdentifyTest { private readonly Client _client; private readonly ITestOutputHelper _outputHelper; public ClientIdentifyTest(ITestOutputHelper helper) { _client = new Client("http", "127.0.0.1", 7450, "55050223-cffb-4d92-b9fe-8a6219e0f87b"); _outputHelper = helper; } [Fact] public async Task IdentifyEvents() { var session1 = await _client.AuthenticateAsync("11111111-0000-0000-0000-000000000000"); var props1 = new Dictionary { { "email", "a@b.com" }, { "pushTokenIos", "foo" } }; var customProps1 = new Dictionary { { "earlyAccess", "true" } }; await _client.UpdatePropertiesAsync(session1, props1, customProps1); var events = new[] { new Event("awardReceived", DateTime.UtcNow), new Event("inventoryUpdated", DateTime.UtcNow) }; await _client.EventsAsync(session1, events); Thread.Sleep(2000); var session2 = await _client.AuthenticateAsync("22222222-0000-0000-0000-000000000000"); var props2 = new Dictionary { { "email", "a@b.com" }, { "pushTokenAndroid", "bar" } }; var customProps2 = new Dictionary { { "earlyAccess", "false" } }; await _client.UpdatePropertiesAsync(session2, props2, customProps2); Thread.Sleep(2000); var session = await _client.IdentifyAsync(session1, "22222222-0000-0000-0000-000000000000", new Dictionary(), new Dictionary()); Assert.NotNull(session); Assert.Equal("22222222-0000-0000-0000-000000000000", session.IdentityId); var props = await _client.ListPropertiesAsync(session); Assert.NotEmpty(props.Default); Assert.NotEmpty(props.Custom); foreach (var prop in props.Default) { switch (prop.Key) { case "email": Assert.Equal("a@b.com", prop.Value); break; case "pushTokenAndroid": Assert.Equal("bar", prop.Value); break; case "pushTokenIos": Assert.Equal("foo", prop.Value); break; } } foreach (var prop in props.Custom) { switch (prop.Key) { case "earlyAccess": Assert.Equal("false", prop.Value); break; } } } } } ================================================ FILE: Satori.Tests/ClientTest.cs ================================================ // Copyright 2022 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Linq; using System.Threading.Tasks; using Xunit; namespace Satori.Tests { public class ClientTest { private const string ApiKey = "55050223-cffb-4d92-b9fe-8a6219e0f87b"; public const int TimeoutMilliseconds = 5000; private readonly Client _testClient = new Client("http", "localhost", 7450, ApiKey, HttpRequestAdapter.WithGzip()); [Fact(Timeout = TimeoutMilliseconds)] public async Task TestAuthenticateAndLogout() { var session = await _testClient.AuthenticateAsync($"{Guid.NewGuid()}"); await _testClient.AuthenticateLogoutAsync(session); await Assert.ThrowsAsync(() => _testClient.GetExperimentsAsync(session, Array.Empty(), Array.Empty())); } [Fact(Timeout = TimeoutMilliseconds)] public async Task TestGetExperiments() { var session = await _testClient.AuthenticateAsync($"{Guid.NewGuid()}"); var experiments = await _testClient.GetAllExperimentsAsync(session); Assert.True(experiments.Experiments.Count() == 1); } [Fact(Timeout = TimeoutMilliseconds)] public async Task TestGetFlags() { var session = await _testClient.AuthenticateAsync($"{Guid.NewGuid()}"); var flags = await _testClient.GetFlagsAsync(session, new string[] { }, Array.Empty()); var excludeHiroFlags = flags.Flags.Where(flag => !flag.Name.StartsWith("Hiro")); Assert.True(excludeHiroFlags.Count() == 4); var namedFlags = await _testClient.GetFlagsAsync(session, new[] { "Min-Build-Number" }, Array.Empty()); Assert.True(namedFlags.Flags.Count() == 1); } [Fact(Timeout = TimeoutMilliseconds)] public async Task TestGetFlagsDefault() { var flags = await _testClient.GetFlagsDefaultAsync(new string[] { }, Array.Empty()); var excludeHiroFlags = flags.Flags.Where(flag => !flag.Name.StartsWith("Hiro")); Assert.True(excludeHiroFlags.Count() == 4); } [Fact(Timeout = TimeoutMilliseconds)] public async Task TestSendEvents() { var session = await _testClient.AuthenticateAsync($"{Guid.NewGuid()}"); await _testClient.EventAsync(session, new Event("gameFinished", DateTime.UtcNow)); await _testClient.EventsAsync(session, new[] { new Event("adStarted", DateTime.UtcNow), new Event("appLaunched", DateTime.UtcNow) }); } [Fact(Timeout = TimeoutMilliseconds)] public async Task TestGetLiveEvent() { var session = await _testClient.AuthenticateAsync($"{Guid.NewGuid()}"); var liveEvents = await _testClient.GetLiveEventsAsync(session); // should not receive any event because not in the targeted audiences Assert.True(!liveEvents.LiveEvents.Any()); } } } ================================================ FILE: Satori.Tests/README.md ================================================ ================================================ FILE: Satori.Tests/Satori.Tests.csproj ================================================ net8.0 enable false 8 runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: Taskfile.dist.yml ================================================ version: '3' dotenv: [ '.env' ] vars: NAKAMA_REPO_URL: 'https://github.com/heroiclabs/nakama.git' SATORI_REPO_URL: 'https://github.com/heroiclabs/satori.git' SEMVER: '{{.SEMVER | default "v3.19.0-pre"}}' tasks: build-debug: cmds: - task: build-debug-nakama - task: build-debug-satori desc: 'Build all artifacts in debug mode.' build-debug-nakama: cmds: - dotnet build ./Nakama/Nakama.csproj desc: 'Build the Nakama client in debug mode.' generates: - 'Nakama/bin/Release/net46' - 'Nakama/bin/Release/netstandard2.0' - 'Nakama/bin/Release/netstandard2.1' sources: - 'Nakama/**/*.cs' build-debug-satori: cmds: - dotnet build ./Satori/Satori.csproj desc: 'Build the Satori client in debug mode.' generates: - 'Satori/bin/Release/net46' - 'Satori/bin/Release/netstandard2.0' - 'Satori/bin/Release/netstandard2.1' sources: - 'Satori/**/*.cs' build-release: cmds: - task: build-release-nakama - task: build-release-satori desc: 'Build all artifacts in release mode.' build-release-nakama: cmds: - dotnet build -c Release ./Nakama/Nakama.csproj desc: 'Build the Nakama client in release mode.' generates: - 'Nakama/bin/Release/net46' - 'Nakama/bin/Release/netstandard2.0' - 'Nakama/bin/Release/netstandard2.1' sources: - 'Nakama/**/*.cs' build-release-satori: cmds: - dotnet build -c Release ./Satori/Satori.csproj desc: 'Build the Satori client in release mode.' generates: - 'Satori/bin/Release/net46' - 'Satori/bin/Release/netstandard2.0' - 'Satori/bin/Release/netstandard2.1' sources: - 'Satori/**/*.cs' codedocs: aliases: [ doxygen ] cmds: - doxygen desc: 'Generate code documentation for both clients.' dir: 'docs' generate: aliases: [ codegen ] cmds: - task: generate-nakama - task: generate-nakamaconsole - task: generate-satori - task: generate-satoriconsole desc: 'Generate all low-level code for the SDK.' generate-nakama: cmds: - go install tool - git clone --depth 1 "{{.NAKAMA_REPO_URL}}" "{{.TMP_DIR}}" - git clone "https://fuchsia.googlesource.com/third_party/googleapis" "{{.TMP_DIR}}/googleapis" - defer: rm -rf "{{.TMP_DIR}}" - | protoc \ -I {{.TMP_DIR}}/apigrpc \ -I {{.TMP_DIR}}/vendor/github.com/heroiclabs/nakama-common \ -I {{.TMP_DIR}}/googleapis \ -I {{.TMP_DIR}}/vendor/github.com/grpc-ecosystem/grpc-gateway/v2 \ --openapiv2_out={{.TMP_DIR}} --openapiv2_opt=logtostderr=true {{.TMP_DIR}}/apigrpc/apigrpc.proto - go run main.go '{{.TMP_DIR}}/apigrpc/apigrpc.swagger.json' 'Nakama' > ../Nakama/ApiClient.gen.cs desc: 'Generate low-level ApiClient for Nakama client.' dir: 'codegen' generates: - '../Nakama/ApiClient.gen.cs' sources: - 'main.go' vars: TMP_DIR: sh: mktemp -d generate-nakamaconsole: cmds: - go install tool - git clone --depth 1 "{{.NAKAMA_REPO_URL}}" "{{.TMP_DIR}}" - git clone "https://fuchsia.googlesource.com/third_party/googleapis" "{{.TMP_DIR}}/googleapis" - defer: rm -rf "{{.TMP_DIR}}" - | protoc \ -I {{.TMP_DIR}} \ -I {{.TMP_DIR}}/vendor \ -I {{.TMP_DIR}}/vendor/github.com/heroiclabs/nakama-common \ -I {{.TMP_DIR}}/googleapis \ -I {{.TMP_DIR}}/build/grpc-gateway-v2.3.0/third_party/googleapis \ -I {{.TMP_DIR}}/vendor/github.com/grpc-ecosystem/grpc-gateway/v2 \ --openapiv2_out={{.TMP_DIR}} --openapiv2_opt=json_names_for_fields=false,logtostderr=true {{.TMP_DIR}}/console/console.proto - go run main.go '{{.TMP_DIR}}/console/console.swagger.json' 'Nakama.Console' > ../Nakama/Console/ConsoleClient.gen.cs desc: 'Generate low-level ConsoleClient for Nakama client.' dir: 'codegen' generates: - '../Nakama/Console/ConsoleClient.gen.cs' sources: - 'main.go' vars: TMP_DIR: sh: mktemp -d generate-satori: cmds: - go install tool - git clone --depth 1 "{{.SATORI_REPO_URL}}" "{{.TMP_DIR}}" - defer: rm -rf "{{.TMP_DIR}}" - | protoc -I {{.TMP_DIR}} -I {{.TMP_DIR}}/vendor \ -I {{.TMP_DIR}}/build/grpc-gateway-v2.3.0/third_party/googleapis \ -I {{.TMP_DIR}}/vendor/github.com/grpc-ecosystem/grpc-gateway/v2 \ --openapiv2_out={{.TMP_DIR}} --openapiv2_opt=logtostderr=true {{.TMP_DIR}}/api/satori.proto - go run main.go '{{.TMP_DIR}}/api/satori.swagger.json' 'Satori' > ../Satori/ApiClient.gen.cs desc: 'Generate low-level ApiClient for Satori client.' dir: 'codegen' generates: - '../Satori/ApiClient.gen.cs' sources: - 'main.go' vars: TMP_DIR: sh: mktemp -d generate-satoriconsole: cmds: - go install tool - git clone --depth 1 "{{.SATORI_REPO_URL}}" "{{.TMP_DIR}}" - defer: rm -rf "{{.TMP_DIR}}" - | protoc -I {{.TMP_DIR}} -I {{.TMP_DIR}}/vendor \ -I {{.TMP_DIR}}/build/grpc-gateway-v2.3.0/third_party/googleapis \ -I {{.TMP_DIR}}/vendor/github.com/grpc-ecosystem/grpc-gateway/v2 \ --openapiv2_out={{.TMP_DIR}} --openapiv2_opt=logtostderr=true {{.TMP_DIR}}/console/console.proto - go run main.go '{{.TMP_DIR}}/console/console.swagger.json' 'Satori.Console' > ../Satori/Console/ConsoleClient.gen.cs desc: 'Generate low-level ConsoleClient for Satori client.' dir: 'codegen' generates: - '../Satori/Console/ConsoleClient.gen.cs' sources: - 'main.go' vars: TMP_DIR: sh: mktemp -d publish-release: cmds: - task: publish-release-nakama - task: publish-release-satori desc: 'Build and publish all artifacts in release mode.' publish-release-nakama: cmds: - task: build-release-nakama - dotnet pack -p:AssemblyVersion={{.SEMVER}} -p:PackageVersion={{.SEMVER}} -c Release ./Nakama/Nakama.csproj - | dotnet nuget push ./Nakama/bin/Release/NakamaClient.{{.SEMVER}}.nupkg \ --api-key {{.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json desc: 'Build and publish the Nakama client in release mode.' vars: SEMVER: '{{.SEMVER | default "3.19.0"}}' publish-release-satori: cmds: - task: build-release-satori - dotnet pack -p:AssemblyVersion={{.SEMVER}} -p:PackageVersion={{.SEMVER}} -c Release ./Satori/Satori.csproj - | dotnet nuget push ./Satori/bin/Release/SatoriClient.{{.SEMVER}}.nupkg \ --api-key {{.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json desc: 'Build and publish the Satori client in release mode.' vars: SEMVER: '{{.SEMVER | default "3.19.0"}}' ================================================ FILE: codegen/README.md ================================================ codegen ======= > A util tool to generate a client from the Swagger spec of Nakama's server API. ### Nakama API To generate a client for Nakama, run the following: ```shell task -v generate-nakama ``` ### Nakama Console API To generate a client for the Nakama Console, run the following: ```shell task -v generate-nakamaconsole ``` ### Satori API To generate a client for Satori, run the following: ```shell task -v generate-satori ``` ### Satori Console API To generate a client for the Satori Console, run the following: ```shell task -v generate-satoriconsole ``` ### Rationale We want to maintain a simple lean low level client within our C# client which has minimal dependencies so we built our own. This gives us complete control over the dependencies required and structure of the code generated. The generated code is designed to be supported within Unity engine, Xamarin, Godot engine, and other projects. It requires .NET4.5 framework, TinyJson, and uses `System.Threading.Tasks`. ### Limitations The code generator has __only__ been checked against the Swagger specification generated for Nakama and Satori servers. YMMV. ================================================ FILE: codegen/go.mod ================================================ module github.com/heroiclabs/nakama-dotnet/codegen go 1.25.0 tool ( github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 google.golang.org/grpc/cmd/protoc-gen-go-grpc google.golang.org/protobuf/cmd/protoc-gen-go ) require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.2-0.20231220213037-30552a56c2c4 // indirect github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: codegen/go.sum ================================================ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.2-0.20231220213037-30552a56c2c4 h1:+sCBciEMFK+JMbP0qqeR4Imj6Y5Y837LZWJrzUx3u1o= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.2-0.20231220213037-30552a56c2c4/go.mod h1:20wXVYqHIgnIKxBBFjWLOWJy2LI8hrhZneJE8MECq5M= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: codegen/main.go ================================================ // Copyright 2020 The Nakama Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bufio" "encoding/json" "flag" "fmt" "os" "strings" "text/template" "unicode" ) const codeTemplate string = `/* Code generated by codegen/main.go. DO NOT EDIT. */ {{- if ne .Namespace "" }} namespace {{.Namespace}} {{- end }} { using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; using TinyJson; /// /// An exception generated for HttpResponse objects don't return a success status. /// public sealed class ApiResponseException : Exception { public long StatusCode { get; } public int GrpcStatusCode { get; } public ApiResponseException(long statusCode, string content, int grpcCode) : base(content) { StatusCode = statusCode; GrpcStatusCode = grpcCode; } public ApiResponseException(string message, Exception e) : base(message, e) { StatusCode = -1L; GrpcStatusCode = -1; } public ApiResponseException(string content) : this(-1L, content, -1) { } public override string ToString() { return $"ApiResponseException(StatusCode={StatusCode}, Message='{Message}', GrpcStatusCode={GrpcStatusCode})"; } } {{- range $defname, $definition := .Definitions }} {{- $classname := $defname | title }} {{- if isRefToEnum $defname }} /// /// {{ $definition.Title | commentify }} /// public enum {{ $classname }} { {{- range $idx, $enum := $definition.Enum }} /// /// {{ (index (splitEnumDescription $definition.Description $idx) $idx) }} /// {{ $enum }} = {{ $idx }}, {{- end }} } {{- else }} /// /// {{ (descriptionOrTitle $definition.Description $definition.Title) | stripNewlines }} /// public interface I{{ $classname }} { {{- range $propname, $property := $definition.Properties }} {{- $fieldname := $propname | snakeToPascal }} /// /// {{ (descriptionOrTitle $property.Description $property.Title) | stripNewlines }} /// {{- if eq $property.Type "integer"}} int {{ $fieldname }} { get; } {{- else if eq $property.Type "number" }} double {{ $fieldname }} { get; } {{- else if eq $property.Type "boolean" }} bool {{ $fieldname }} { get; } {{- else if eq $property.Type "string"}} string {{ $fieldname }} { get; } {{- else if eq $property.Type "array"}} {{- if eq $property.Items.Type "string"}} List {{ $fieldname }} { get; } {{- else if eq $property.Items.Type "integer"}} List {{ $fieldname }} { get; } {{- else if eq $property.Items.Type "number"}} List {{ $fieldname }} { get; } {{- else if eq $property.Items.Type "boolean"}} List {{ $fieldname }} { get; } {{- else}} IEnumerable {{ $fieldname }} { get; } {{- end }} {{- else if eq $property.Type "object"}} {{- if eq $property.AdditionalProperties.Type "string" }} {{- if eq $property.AdditionalProperties.Format "int64" }} IDictionary {{$fieldname}} { get; } {{- else }} IDictionary {{$fieldname}} { get; } {{- end }} {{- else if eq $property.AdditionalProperties.Type "integer"}} IDictionary {{$fieldname}} { get; } {{- else if eq $property.AdditionalProperties.Type "number"}} IDictionary {{$fieldname}} { get; } {{- else if eq $property.AdditionalProperties.Type "boolean"}} IDictionary {{$fieldname}} { get; } {{- else }} IDictionary {{$fieldname}} { get; } {{- end}} {{- else if isRefToEnum (cleanRef $property.Ref) }} {{ $property.Ref | cleanRef }} {{ $fieldname }} { get; } {{- else }} I{{ $property.Ref | cleanRef }} {{ $fieldname }} { get; } {{- end }} {{- end }} } /// internal class {{ $classname }} : I{{ $classname }} { {{- range $propname, $property := $definition.Properties }} {{- $fieldname := $propname | snakeToPascal }} {{- $attrDataName := $propname | camelToSnake }} /// {{- if eq $property.Type "integer" }} [DataMember(Name="{{ $attrDataName }}"), Preserve] public int {{ $fieldname }} { get; set; } {{- else if eq $property.Type "number" }} [DataMember(Name="{{ $attrDataName }}"), Preserve] public double {{ $fieldname }} { get; set; } {{- else if eq $property.Type "boolean" }} [DataMember(Name="{{ $attrDataName }}"), Preserve] public bool {{ $fieldname }} { get; set; } {{- else if eq $property.Type "string" }} [DataMember(Name="{{ $attrDataName }}"), Preserve] public string {{ $fieldname }} { get; set; } {{- else if eq $property.Type "array" }} {{- if eq $property.Items.Type "string" }} [DataMember(Name="{{ $attrDataName }}"), Preserve] public List {{ $fieldname }} { get; set; } {{- else if eq $property.Items.Type "integer" }} [DataMember(Name="{{ $propname }}"), Preserve] public List {{ $fieldname }} { get; set; } {{- else if eq $property.Items.Type "number" }} [DataMember(Name="{{ $attrDataName }}"), Preserve] public List {{ $fieldname }} { get; set; } {{- else if eq $property.Items.Type "boolean" }} [DataMember(Name="{{ $attrDataName }}"), Preserve] public List {{ $fieldname }} { get; set; } {{- else}} [IgnoreDataMember] public IEnumerable {{ $fieldname }} => _{{ $propname | snakeToCamel }} ?? new List<{{ $property.Items.Ref | cleanRef }}>(0); [DataMember(Name="{{ $attrDataName }}"), Preserve] public List<{{ $property.Items.Ref | cleanRef }}> _{{ $propname | snakeToCamel }} { get; set; } {{- end }} {{- else if eq $property.Type "object"}} {{- if eq $property.AdditionalProperties.Type "string"}} {{- if eq $property.AdditionalProperties.Format "int64" }} [IgnoreDataMember] public IDictionary {{ $fieldname }} => ApiClient.DeserializeIntProperties(_{{ $propname | snakeToCamel }}) ?? new Dictionary(); [DataMember(Name="{{ $attrDataName }}"), Preserve] public Dictionary _{{ $propname | snakeToCamel }} { get; set; } {{- else }} [IgnoreDataMember] public IDictionary {{ $fieldname }} => _{{ $propname | snakeToCamel }} ?? new Dictionary(); [DataMember(Name="{{ $attrDataName }}"), Preserve] public Dictionary _{{ $propname | snakeToCamel }} { get; set; } {{- end }} {{- else if eq $property.AdditionalProperties.Type "integer"}} [IgnoreDataMember] public IDictionary {{ $fieldname }} => _{{ $propname | snakeToCamel }} ?? new Dictionary(); [DataMember(Name="{{ $attrDataName }}"), Preserve] {{- else if eq $property.AdditionalProperties.Type "number"}} [IgnoreDataMember] public IDictionary {{ $fieldname }} => _{{ $propname | snakeToCamel }} ?? new Dictionary(); [DataMember(Name="{{ $attrDataName }}"), Preserve] public Dictionary _{{ $propname | snakeToCamel }} { get; set; } {{- else if eq $property.AdditionalProperties.Type "boolean"}} [IgnoreDataMember] public IDictionary {{ $fieldname }} => _{{ $propname | snakeToCamel }} ?? new Dictionary(); [DataMember(Name="{{ $attrDataName }}"), Preserve] public Dictionary _{{ $propname | snakeToCamel }} { get; set; } {{- else}} [IgnoreDataMember] public IDictionary {{ $fieldname }} => _{{ $propname | snakeToCamel }} ?? new Dictionary(); [DataMember(Name="{{ $attrDataName }}"), Preserve] public Dictionary _{{ $propname | snakeToCamel }} { get; set; } {{- end}} {{- else if isRefToEnum (cleanRef $property.Ref) }} [IgnoreDataMember] public {{ $property.Ref | cleanRef }} {{ $fieldname }} => _{{ $propname | snakeToCamel }}; [DataMember(Name="{{ $attrDataName }}"), Preserve] public {{ $property.Ref | cleanRef }} _{{ $propname | snakeToCamel }} { get; set; } {{- else }} [IgnoreDataMember] public I{{ $property.Ref | cleanRef }} {{ $fieldname }} => _{{ $propname | snakeToCamel }}; [DataMember(Name="{{ $attrDataName }}"), Preserve] public {{ $property.Ref | cleanRef }} _{{ $propname | snakeToCamel }} { get; set; } {{- end }} {{- end }} public override string ToString() { var output = ""; {{- range $fieldname, $property := $definition.Properties }} {{- if eq $property.Type "array" }} output = string.Concat(output, "{{ $fieldname | snakeToPascal }}: [", string.Join(", ", {{ $fieldname | snakeToPascal }}), "], "); {{- else if eq $property.Type "object" }} var {{ $fieldname }}String = ""; foreach (var kvp in {{ $fieldname | snakeToPascal }}) { {{ $fieldname }}String = string.Concat({{ $fieldname }}String, "{" + kvp.Key + "=" + kvp.Value + "}"); } output = string.Concat(output, "{{ $fieldname | snakeToPascal }}: [" + {{ $fieldname }}String + "]"); {{- else }} output = string.Concat(output, "{{ $fieldname | snakeToPascal }}: ", {{ $fieldname | snakeToPascal }}, ", "); {{- end }} {{- end }} return output; } } {{- end }} {{- end }} /// /// The low level client for the {{ .Namespace }} API. /// internal class ApiClient { public readonly IHttpAdapter HttpAdapter; public int Timeout { get; set; } private readonly Uri _baseUri; public ApiClient(Uri baseUri, IHttpAdapter httpAdapter, int timeout = 10) { _baseUri = baseUri; HttpAdapter = httpAdapter; Timeout = timeout; } {{- range $url, $path := .Paths }} {{- range $method, $operation := $path}} /// /// {{ $operation.Summary | stripNewlines }} /// {{- if $operation.Responses.Ok.Schema.Ref }} public async Task {{ $operation.OperationId | stripOperationPrefix | snakeToPascal }}Async( {{- else }} public async Task {{ $operation.OperationId | stripOperationPrefix | snakeToPascal }}Async( {{- end}} {{- $isPreviousParam := false}} {{- if $operation.Security }} {{- range $idx, $security := $operation.Security}} {{- range $key, $value := $security}} {{- if or (eq $key "BasicAuth") (eq $key "HttpKeyAuth") }} string basicAuthUsername, string basicAuthPassword {{- $isPreviousParam = true}} {{- else if (eq $key "BearerJwt") }} {{- if eq $isPreviousParam true}},{{- end}} {{- $isPreviousParam = true}} string bearerToken {{- end }} {{- end }} {{- end }} {{- else }} {{- if eq $isPreviousParam true}},{{- end}} {{- $isPreviousParam = true}} string bearerToken {{- end }} {{- range $parameter := $operation.Parameters }} {{- if eq $isPreviousParam true}},{{- end}} {{- if eq $parameter.In "path" }} {{ $parameter.Type }}{{- if not $parameter.Required }}?{{- end }} {{ $parameter.Name | snakeToCamel}} {{- else if eq $parameter.In "body" }} {{- if eq $parameter.Schema.Type "string" }} string{{- if not $parameter.Required }}?{{- end }} {{ $parameter.Name | snakeToCamel}} {{- else }} {{ $parameter.Schema.Ref | cleanRef }}{{- if not $parameter.Required }}?{{- end }} {{ $parameter.Name | snakeToCamel}} {{- end }} {{- else if eq $parameter.Type "array"}} IEnumerable<{{ $parameter.Items.Type }}> {{ $parameter.Name | snakeToCamel }} {{- else if eq $parameter.Type "object"}} {{- if eq $parameter.AdditionalProperties.Type "string"}} IDictionary {{ $parameter.Name }} {{- else if eq $parameter.Items.Type "integer"}} IDictionary {{ $parameter.Name }} {{- else if eq $parameter.Items.Type "boolean"}} IDictionary {{ $parameter.Name }} {{- else}} IDictionary {{ $parameter.Name }} {{- end}} {{- else if eq $parameter.Type "integer" }} int? {{ $parameter.Name }} {{- else if eq $parameter.Type "boolean" }} bool? {{ $parameter.Name }} {{- else if eq $parameter.Type "string" }} string {{ $parameter.Name | snakeToCamel}} {{- else }} {{ $parameter.Type }} {{ $parameter.Name | snakeToCamel}} {{- end }} {{- $isPreviousParam = true}} {{- end }}, CancellationToken? cancellationToken) { {{- range $parameter := $operation.Parameters }} {{- if $parameter.Required }} if ({{ $parameter.Name | snakeToCamel}} == null) { throw new ArgumentException("'{{ $parameter.Name | snakeToCamel }}' is required but was null."); } {{- end }} {{- end }} var urlpath = "{{- $url }}"; {{- range $parameter := $operation.Parameters }} {{- $camelToSnake := $parameter.Name | camelToSnake }} {{- if eq $parameter.In "path" }} urlpath = urlpath.Replace("{{- print "{" $parameter.Name "}"}}", Uri.EscapeDataString({{- $parameter.Name | snakeToCamel }})); {{- end }} {{- end }} var queryParams = ""; {{- range $parameter := $operation.Parameters }} {{- $camelToSnake := $parameter.Name | camelToSnake }} {{- if eq $parameter.In "query"}} {{- if eq $parameter.Type "integer" }} if ({{ $parameter.Name }} != null) { queryParams = string.Concat(queryParams, "{{- $camelToSnake }}=", {{ $parameter.Name }}, "&"); } {{- else if eq $parameter.Type "string" }} if ({{ $parameter.Name | snakeToCamel }} != null) { queryParams = string.Concat(queryParams, "{{- $camelToSnake }}=", Uri.EscapeDataString({{ $parameter.Name | snakeToCamel }}), "&"); } {{- else if eq $parameter.Type "boolean" }} if ({{ $parameter.Name }} != null) { queryParams = string.Concat(queryParams, "{{- $camelToSnake }}=", {{ $parameter.Name }}.ToString().ToLower(), "&"); } {{- else if eq $parameter.Type "array" }} foreach (var elem in {{ $parameter.Name | snakeToCamel }} ?? new {{ $parameter.Items.Type }}[0]) { {{- if eq $parameter.Items.Type "string" }} queryParams = string.Concat(queryParams, "{{- $camelToSnake }}=", Uri.EscapeDataString(elem), "&"); {{- else }} queryParams = string.Concat(queryParams, "{{- $camelToSnake }}=", elem, "&"); {{- end }} } {{- else }} {{ $parameter }} // ERROR {{- end }} {{- end }} {{- end }} string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; var uri = new UriBuilder(_baseUri) { Path = path, Query = queryParams }.Uri; var httpMethod = "{{- $method | uppercase }}"; var headers = new Dictionary(); {{- if $operation.Security }} {{- range $idx, $security := $operation.Security }} {{- range $key, $value := $security }} {{- if or (eq $key "BasicAuth") (eq $key "HttpKeyAuth")}} if (!string.IsNullOrEmpty(basicAuthUsername)) { var credentials = Encoding.UTF8.GetBytes(basicAuthUsername + ":" + basicAuthPassword); var header = string.Concat("Basic ", Convert.ToBase64String(credentials)); headers.Add("Authorization", header); } {{- else if (eq $key "BearerJwt") }} if (!string.IsNullOrEmpty(bearerToken)) { var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); } {{- end }} {{- end }} {{- end }} {{- else }} var header = string.Concat("Bearer ", bearerToken); headers.Add("Authorization", header); {{- end }} byte[] content = null; {{- range $parameter := $operation.Parameters }} {{- if eq $parameter.In "body" }} var jsonBody = {{ $parameter.Name }}.ToJson(); content = Encoding.UTF8.GetBytes(jsonBody); {{- end }} {{- end }} {{- if $operation.Responses.Ok.Schema.Ref }} var contents = await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); return contents.FromJson<{{ $operation.Responses.Ok.Schema.Ref | cleanRef }}>(); {{- else }} await HttpAdapter.SendAsync(httpMethod, uri, headers, content, Timeout, cancellationToken); {{- end }} } {{- end }} {{- end }} } } ` func convertRefToClassName(input string) (className string) { cleanRef := strings.TrimPrefix(input, "#/definitions/") className = strings.Title(cleanRef) return } // camelToSnake converts a camel or Pascal case string into snake case. func camelToSnake(input string) (output string) { for k, v := range input { if unicode.IsUpper(v) { formatString := "%c" if k != 0 { formatString = "_" + formatString } output += fmt.Sprintf(formatString, unicode.ToLower(v)) } else { output += string(v) } } return } func snakeToCamel(input string) (snakeToCamel string) { isToUpper := false for k, v := range input { if k == 0 { snakeToCamel = strings.ToLower(string(input[0])) } else { if isToUpper { snakeToCamel += strings.ToUpper(string(v)) isToUpper = false } else { if v == '_' { isToUpper = true } else { snakeToCamel += string(v) } } } } return } func snakeToPascal(input string) (output string) { isToUpper := false for k, v := range input { if k == 0 { output = strings.ToUpper(string(input[0])) } else { if isToUpper { output += strings.ToUpper(string(v)) isToUpper = false } else { if v == '_' { isToUpper = true } else { output += string(v) } } } } return } func isPropertyEnum(string) (output string) { return } // pascalToCamel converts a Pascal case string to a camel case string. func pascalToCamel(input string) (camelCase string) { if input == "" { return "" } camelCase = strings.ToLower(string(input[0])) camelCase += string(input[1:]) return camelCase } func splitEnumDescription(description string, idx int) []string { if description == "" { return make([]string, idx+1) } tokens := strings.Split(description, "\n") if len(tokens)-1 < idx { return make([]string, idx+1) } return tokens } func stripNewlines(input string) string { return strings.Replace(input, "\n", " ", -1) } func stripOperationPrefix(input string) string { return strings.Replace(input, "Nakama_", "", 1) } func descriptionOrTitle(description string, title string) string { if description != "" { return description } return title } func commentify(input string) string { return strings.Replace(input, "\n", "\n /// ", -1) } // camelToPascal converts a string from camel case to Pascal case. func camelToPascal(camelCase string) (pascalCase string) { if len(camelCase) <= 0 { return "" } pascalCase = strings.ToUpper(string(camelCase[0])) + camelCase[1:] return } func main() { // Argument flags var output = flag.String("output", "", "The output for generated code.") flag.Parse() inputs := flag.Args() if len(inputs) < 1 { fmt.Printf("No input file found: %s\n\n", inputs) fmt.Println("openapi-gen [flags] inputs...") flag.PrintDefaults() return } inputFile := inputs[0] content, err := os.ReadFile(inputFile) if err != nil { fmt.Printf("Unable to read file: %s\n", err) return } var namespace (string) = "" if len(inputs) > 1 { if len(inputs[1]) <= 0 { fmt.Println("Empty Namespace provided.") return } namespace = inputs[1] } var schema *Schema if err := json.Unmarshal(content, &schema); err != nil { fmt.Printf("Unable to decode input file %s : %s\n", inputFile, err) return } schema.Namespace = namespace generateBodyDefinitionFromSchema(schema) fmap := template.FuncMap{ "snakeToCamel": snakeToCamel, "camelToSnake": camelToSnake, "cleanRef": convertRefToClassName, "isRefToEnum": func(ref string) bool { // swagger schema definition keys have inconsistent casing var camelOk bool var pascalOk bool var enums []string asCamel := pascalToCamel(ref) if _, camelOk = schema.Definitions[asCamel]; camelOk { enums = schema.Definitions[asCamel].Enum } asPascal := camelToPascal(ref) if _, pascalOk = schema.Definitions[asPascal]; pascalOk { enums = schema.Definitions[asPascal].Enum } if !pascalOk && !camelOk { fmt.Printf("no definition found: %v", ref) return false } return len(enums) > 0 }, "pascalToCamel": pascalToCamel, "snakeToPascal": snakeToPascal, "stripNewlines": stripNewlines, "title": strings.Title, "uppercase": strings.ToUpper, "camelToPascal": camelToPascal, "splitEnumDescription": splitEnumDescription, "stripOperationPrefix": stripOperationPrefix, "descriptionOrTitle": descriptionOrTitle, "commentify": commentify, } tmpl, err := template.New(inputFile).Funcs(fmap).Parse(codeTemplate) if err != nil { panic(err) } if len(*output) < 1 { if err := tmpl.Execute(os.Stdout, schema); err != nil { panic(err) } return } f, err := os.Create(*output) if err != nil { fmt.Printf("Unable to create file: %s\n", err) return } defer f.Close() writer := bufio.NewWriter(f) tmpl.Execute(writer, schema) writer.Flush() } type Schema struct { Namespace string Paths map[string]map[string]struct { Summary string OperationId string Responses struct { Ok struct { Schema struct { Ref string `json:"$ref"` } } `json:"200"` } Parameters []struct { Name string In string Required bool Type string // used with primitives Items struct { // used with type "array" Type string } Format string // used with type "boolean" Schema ObjectSchema `json:"schema"` } Security []map[string][]struct { } } Definitions map[string]ObjectDefinition } type ObjectSchema struct { Type string Ref string `json:"$ref"` Properties map[string]struct { Type string Description string } Description string } type ObjectDefinition struct { Properties map[string]ObjectProperty Enum []string Description string // used only by enums Title string } type ObjectProperty struct { Type string Ref string `json:"$ref"` // used with object Items Items AdditionalProperties AdditionalProperties Format string // used with type "boolean" Description string Title string // used by enums } type Items struct { Type string Ref string `json:"$ref"` } type AdditionalProperties struct { Type string // used with type "map" Format string // used with type "map" Ref string `json:"$ref"` // used with object } func generateBodyDefinitionFromSchema(s *Schema) { // Needed because of this change: https://github.com/grpc-ecosystem/grpc-gateway/issues/1670 for _, path := range s.Paths { // Iterate through each HTTP method (e.g., "get", "post", "put") for the current path for verb, operation := range path { // Check if the HTTP method is one that can contain a body if verb == "post" || verb == "put" { // Iterate through the parameters of the operation for idx, param := range operation.Parameters { // Check if the parameter is a body parameter with an inline schema if param.In == "body" && param.Name == "body" && param.Schema.Ref == "" { // Construct a unique name for the new definition objectName := "Api" + strings.TrimPrefix(operation.OperationId, fmt.Sprintf("%s_", s.Namespace)) + "Request" // Update the parameter's schema reference to the new definition param.Schema.Ref = "#/definitions/" + objectName // Update the parameter in the original operation object operation.Parameters[idx] = param // Create the new definition properties := make(map[string]ObjectProperty) for key, p := range param.Schema.Properties { properties[key] = ObjectProperty{ Type: p.Type, Items: Items{}, AdditionalProperties: AdditionalProperties{ Type: "string", }, Description: p.Description, } } // Add the new definition to the schema's definitions map s.Definitions[objectName] = ObjectDefinition{ Properties: properties, Description: param.Schema.Description, } } } } } } } ================================================ FILE: docs/.nojekyll ================================================ ================================================ FILE: docs/CNAME ================================================ dotnet.docs.heroiclabs.com ================================================ FILE: docs/Doxyfile ================================================ # Doxyfile 1.9.2 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. # # All text after a double hash (##) is considered a comment and is placed in # front of the TAG it is preceding. # # All text after a single hash (#) is considered a comment and will be ignored. # The format is: # TAG = value [value, ...] # For lists, items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (\" \"). #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- # This tag specifies the encoding used for all characters in the configuration # file that follow. The default is UTF-8 which is also the encoding used for all # text before the first occurrence of this tag. Doxygen uses libiconv (or the # iconv built into libc) for the transcoding. See # https://www.gnu.org/software/libiconv/ for the list of possible encodings. # The default value is: UTF-8. DOXYFILE_ENCODING = UTF-8 # The PROJECT_NAME tag is a single word (or a sequence of words surrounded by # double-quotes, unless you are using Doxywizard) that should identify the # project for which the documentation is generated. This name is used in the # title of most generated pages and in a few other places. # The default value is: My Project. PROJECT_NAME = "Nakama .NET Client" # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version # control system is used. PROJECT_NUMBER = 3.17.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a # quick idea about the purpose of the project. Keep the description short. PROJECT_BRIEF = "The official Nakama and Satori .NET client." # With the PROJECT_LOGO tag one can specify a logo or an icon that is included # in the documentation. The maximum height of the logo should not exceed 55 # pixels and the maximum width should not exceed 200 pixels. Doxygen will copy # the logo to the output directory. PROJECT_LOGO = ./nakama_logo.svg # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # into which the generated documentation will be written. If a relative path is # entered, it will be relative to the location where doxygen was started. If # left blank the current directory will be used. OUTPUT_DIRECTORY = . # If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- # directories (in 2 levels) under the output directory of each output format and # will distribute the generated files over these directories. Enabling this # option can be useful when feeding doxygen a huge amount of source files, where # putting all generated files in the same directory would otherwise causes # performance problems for the file system. # The default value is: NO. CREATE_SUBDIRS = NO # If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII # characters to appear in the names of generated files. If set to NO, non-ASCII # characters will be escaped, for example _xE3_x81_x84 will be used for Unicode # U+3044. # The default value is: NO. ALLOW_UNICODE_NAMES = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. # Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, # Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), # Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, # Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), # Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, # Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, # Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, # Ukrainian and Vietnamese. # The default value is: English. OUTPUT_LANGUAGE = English # If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member # descriptions after the members that are listed in the file and class # documentation (similar to Javadoc). Set to NO to disable this. # The default value is: YES. BRIEF_MEMBER_DESC = YES # If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief # description of a member or function before the detailed description # # Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the # brief descriptions will be completely suppressed. # The default value is: YES. REPEAT_BRIEF = YES # This tag implements a quasi-intelligent brief description abbreviator that is # used to form the text in various listings. Each string in this list, if found # as the leading text of the brief description, will be stripped from the text # and the result, after processing the whole list, is used as the annotated # text. Otherwise, the brief description is used as-is. If left blank, the # following values are used ($name is automatically replaced with the name of # the entity):The $name class, The $name widget, The $name file, is, provides, # specifies, contains, represents, a, an and the. ABBREVIATE_BRIEF = "The $name class" \ "The $name widget" \ "The $name file" \ is \ provides \ specifies \ contains \ represents \ a \ an \ the # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then # doxygen will generate a detailed section even if there is only a brief # description. # The default value is: NO. ALWAYS_DETAILED_SEC = NO # If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. # The default value is: NO. INLINE_INHERITED_MEMB = NO # If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path # before files name in the file list and in the header files. If set to NO the # shortest path that makes the file name unique will be used # The default value is: YES. FULL_PATH_NAMES = NO # The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. # Stripping is only done if one of the specified strings matches the left-hand # part of the path. The tag can be used to show relative paths in the file list. # If left blank the directory from which doxygen is run is used as the path to # strip. # # Note that you can specify absolute paths here, but also relative paths, which # will be relative from the directory where doxygen is started. # This tag requires that the tag FULL_PATH_NAMES is set to YES. STRIP_FROM_PATH = # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the # path mentioned in the documentation of a class, which tells the reader which # header file to include in order to use a class. If left blank only the name of # the header file containing the class definition is used. Otherwise one should # specify the list of include paths that are normally passed to the compiler # using the -I flag. STRIP_FROM_INC_PATH = # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but # less readable) file names. This can be useful is your file systems doesn't # support long names like on DOS, Mac, or CD-ROM. # The default value is: NO. SHORT_NAMES = NO # If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the # first line (until the first dot) of a Javadoc-style comment as the brief # description. If set to NO, the Javadoc-style will behave just like regular Qt- # style comments (thus requiring an explicit @brief command for a brief # description.) # The default value is: NO. JAVADOC_AUTOBRIEF = NO # If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line # such as # /*************** # as being the beginning of a Javadoc-style comment "banner". If set to NO, the # Javadoc-style will behave just like regular comments and it will not be # interpreted by doxygen. # The default value is: NO. JAVADOC_BANNER = NO # If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first # line (until the first dot) of a Qt-style comment as the brief description. If # set to NO, the Qt-style will behave just like regular Qt-style comments (thus # requiring an explicit \brief command for a brief description.) # The default value is: NO. QT_AUTOBRIEF = NO # The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a # multi-line C++ special comment block (i.e. a block of //! or /// comments) as # a brief description. This used to be the default behavior. The new default is # to treat a multi-line C++ comment block as a detailed description. Set this # tag to YES if you prefer the old behavior instead. # # Note that setting this tag to YES also means that rational rose comments are # not recognized any more. # The default value is: NO. MULTILINE_CPP_IS_BRIEF = NO # By default Python docstrings are displayed as preformatted text and doxygen's # special commands cannot be used. By setting PYTHON_DOCSTRING to NO the # doxygen's special commands can be used and the contents of the docstring # documentation blocks is shown as doxygen documentation. # The default value is: YES. PYTHON_DOCSTRING = YES # If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the # documentation from any documented member that it re-implements. # The default value is: YES. INHERIT_DOCS = YES # If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new # page for each member. If set to NO, the documentation of a member will be part # of the file/class/namespace that contains it. # The default value is: NO. SEPARATE_MEMBER_PAGES = NO # The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen # uses this value to replace tabs by spaces in code fragments. # Minimum value: 1, maximum value: 16, default value: 4. TAB_SIZE = 4 # This tag can be used to specify a number of aliases that act as commands in # the documentation. An alias has the form: # name=value # For example adding # "sideeffect=@par Side Effects:^^" # will allow you to put the command \sideeffect (or @sideeffect) in the # documentation, which will result in a user-defined paragraph with heading # "Side Effects:". Note that you cannot put \n's in the value part of an alias # to insert newlines (in the resulting output). You can put ^^ in the value part # of an alias to insert a newline as if a physical newline was in the original # file. When you need a literal { or } or , in the value part of an alias you # have to escape them by means of a backslash (\), this can lead to conflicts # with the commands \{ and \} for these it is advised to use the version @{ and # @} or use a double escape (\\{ and \\}) ALIASES = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For # instance, some of the names that are used will be different. The list of all # members will be omitted, etc. # The default value is: NO. OPTIMIZE_OUTPUT_FOR_C = NO # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or # Python sources only. Doxygen will then generate output that is more tailored # for that language. For instance, namespaces will be presented as packages, # qualified scopes will look different, etc. # The default value is: NO. OPTIMIZE_OUTPUT_JAVA = NO # Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran # sources. Doxygen will then generate output that is tailored for Fortran. # The default value is: NO. OPTIMIZE_FOR_FORTRAN = NO # Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL # sources. Doxygen will then generate output that is tailored for VHDL. # The default value is: NO. OPTIMIZE_OUTPUT_VHDL = NO # Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice # sources only. Doxygen will then generate output that is more tailored for that # language. For instance, namespaces will be presented as modules, types will be # separated into more groups, etc. # The default value is: NO. OPTIMIZE_OUTPUT_SLICE = NO # Doxygen selects the parser to use depending on the extension of the files it # parses. With this tag you can assign which parser to use for a given # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, and # language is one of the parsers supported by doxygen: IDL, Java, JavaScript, # Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, # VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: # FortranFree, unknown formatted Fortran: Fortran. In the later case the parser # tries to guess whether the code is fixed or free formatted code, this is the # default for Fortran type files). For instance to make doxygen treat .inc files # as Fortran files (default is PHP), and .f files as C (default is Fortran), # use: inc=Fortran f=C. # # Note: For files without extension you can use no_extension as a placeholder. # # Note that for custom extensions you also need to set FILE_PATTERNS otherwise # the files are not read by doxygen. When specifying no_extension you should add # * to the FILE_PATTERNS. # # Note see also the list of default file extension mappings. EXTENSION_MAPPING = # If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments # according to the Markdown format, which allows for more readable # documentation. See https://daringfireball.net/projects/markdown/ for details. # The output of markdown processing is further processed by doxygen, so you can # mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in # case of backward compatibilities issues. # The default value is: YES. MARKDOWN_SUPPORT = YES # When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up # to that level are automatically included in the table of contents, even if # they do not have an id attribute. # Note: This feature currently applies only to Markdown headings. # Minimum value: 0, maximum value: 99, default value: 5. # This tag requires that the tag MARKDOWN_SUPPORT is set to YES. TOC_INCLUDE_HEADINGS = 0 # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or # globally by setting AUTOLINK_SUPPORT to NO. # The default value is: YES. AUTOLINK_SUPPORT = YES # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # to include (a tag file for) the STL sources as input, then you should set this # tag to YES in order to let doxygen match functions declarations and # definitions whose arguments contain STL classes (e.g. func(std::string); # versus func(std::string) {}). This also make the inheritance and collaboration # diagrams that involve STL classes more complete and accurate. # The default value is: NO. BUILTIN_STL_SUPPORT = NO # If you use Microsoft's C++/CLI language, you should set this option to YES to # enable parsing support. # The default value is: NO. CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip (see: # https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen # will parse them like normal C++ but will assume all classes use public instead # of private inheritance when no explicit protection keyword is present. # The default value is: NO. SIP_SUPPORT = NO # For Microsoft's IDL there are propget and propput attributes to indicate # getter and setter methods for a property. Setting this option to YES will make # doxygen to replace the get and set methods by a property in the documentation. # This will only work if the methods are indeed getting or setting a simple # type. If this is not the case, or you want to show the methods anyway, you # should set this option to NO. # The default value is: YES. IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC # tag is set to YES then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. # The default value is: NO. DISTRIBUTE_GROUP_DOC = NO # If one adds a struct or class to a group and this option is enabled, then also # any nested class or struct is added to the same group. By default this option # is disabled and one has to add nested compounds explicitly via \ingroup. # The default value is: NO. GROUP_NESTED_COMPOUNDS = NO # Set the SUBGROUPING tag to YES to allow class member groups of the same type # (for instance a group of public functions) to be put as a subgroup of that # type (e.g. under the Public Functions section). Set it to NO to prevent # subgrouping. Alternatively, this can be done per class using the # \nosubgrouping command. # The default value is: YES. SUBGROUPING = YES # When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions # are shown inside the group in which they are included (e.g. using \ingroup) # instead of on a separate page (for HTML and Man pages) or section (for LaTeX # and RTF). # # Note that this feature does not work in combination with # SEPARATE_MEMBER_PAGES. # The default value is: NO. INLINE_GROUPED_CLASSES = NO # When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions # with only public data fields or simple typedef fields will be shown inline in # the documentation of the scope in which they are defined (i.e. file, # namespace, or group documentation), provided this scope is documented. If set # to NO, structs, classes, and unions are shown on a separate page (for HTML and # Man pages) or section (for LaTeX and RTF). # The default value is: NO. INLINE_SIMPLE_STRUCTS = NO # When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or # enum is documented as struct, union, or enum with the name of the typedef. So # typedef struct TypeS {} TypeT, will appear in the documentation as a struct # with name TypeT. When disabled the typedef will appear as a member of a file, # namespace, or class. And the struct will be named TypeS. This can typically be # useful for C code in case the coding convention dictates that all compound # types are typedef'ed and only the typedef is referenced, never the tag name. # The default value is: NO. TYPEDEF_HIDES_STRUCT = NO # The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This # cache is used to resolve symbols given their name and scope. Since this can be # an expensive process and often the same symbol appears multiple times in the # code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small # doxygen will become slower. If the cache is too large, memory is wasted. The # cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range # is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 # symbols. At the end of a run doxygen will report the cache usage and suggest # the optimal cache size from a speed point of view. # Minimum value: 0, maximum value: 9, default value: 0. LOOKUP_CACHE_SIZE = 0 # The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use # during processing. When set to 0 doxygen will based this on the number of # cores available in the system. You can set it explicitly to a value larger # than 0 to get more control over the balance between CPU load and processing # speed. At this moment only the input processing can be done using multiple # threads. Since this is still an experimental feature the default is set to 1, # which effectively disables parallel processing. Please report any issues you # encounter. Generating dot graphs in parallel is controlled by the # DOT_NUM_THREADS setting. # Minimum value: 0, maximum value: 32, default value: 1. NUM_PROC_THREADS = 1 #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- # If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in # documentation are documented, even if no documentation was available. Private # class members and static file members will be hidden unless the # EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. # Note: This will also disable the warnings about undocumented members that are # normally produced when WARNINGS is set to YES. # The default value is: NO. EXTRACT_ALL = YES # If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will # be included in the documentation. # The default value is: NO. EXTRACT_PRIVATE = NO # If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual # methods of a class will be included in the documentation. # The default value is: NO. EXTRACT_PRIV_VIRTUAL = NO # If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal # scope will be included in the documentation. # The default value is: NO. EXTRACT_PACKAGE = NO # If the EXTRACT_STATIC tag is set to YES, all static members of a file will be # included in the documentation. # The default value is: NO. EXTRACT_STATIC = NO # If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined # locally in source files will be included in the documentation. If set to NO, # only classes defined in header files are included. Does not have any effect # for Java sources. # The default value is: YES. EXTRACT_LOCAL_CLASSES = YES # This flag is only useful for Objective-C code. If set to YES, local methods, # which are defined in the implementation section but not in the interface are # included in the documentation. If set to NO, only methods in the interface are # included. # The default value is: NO. EXTRACT_LOCAL_METHODS = NO # If this flag is set to YES, the members of anonymous namespaces will be # extracted and appear in the documentation as a namespace called # 'anonymous_namespace{file}', where file will be replaced with the base name of # the file that contains the anonymous namespace. By default anonymous namespace # are hidden. # The default value is: NO. EXTRACT_ANON_NSPACES = NO # If this flag is set to YES, the name of an unnamed parameter in a declaration # will be determined by the corresponding definition. By default unnamed # parameters remain unnamed in the output. # The default value is: YES. RESOLVE_UNNAMED_PARAMS = YES # If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all # undocumented members inside documented classes or files. If set to NO these # members will be included in the various overviews, but no documentation # section is generated. This option has no effect if EXTRACT_ALL is enabled. # The default value is: NO. HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. If set # to NO, these classes will be included in the various overviews. This option # has no effect if EXTRACT_ALL is enabled. # The default value is: NO. HIDE_UNDOC_CLASSES = NO # If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend # declarations. If set to NO, these declarations will be included in the # documentation. # The default value is: NO. HIDE_FRIEND_COMPOUNDS = NO # If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any # documentation blocks found inside the body of a function. If set to NO, these # blocks will be appended to the function's detailed documentation block. # The default value is: NO. HIDE_IN_BODY_DOCS = NO # The INTERNAL_DOCS tag determines if documentation that is typed after a # \internal command is included. If the tag is set to NO then the documentation # will be excluded. Set it to YES to include the internal documentation. # The default value is: NO. INTERNAL_DOCS = NO # With the correct setting of option CASE_SENSE_NAMES doxygen will better be # able to match the capabilities of the underlying filesystem. In case the # filesystem is case sensitive (i.e. it supports files in the same directory # whose names only differ in casing), the option must be set to YES to properly # deal with such files in case they appear in the input. For filesystems that # are not case sensitive the option should be be set to NO to properly deal with # output files written for symbols that only differ in casing, such as for two # classes, one named CLASS and the other named Class, and to also support # references to files without having to specify the exact matching casing. On # Windows (including Cygwin) and MacOS, users should typically set this option # to NO, whereas on Linux or other Unix flavors it should typically be set to # YES. # The default value is: system dependent. CASE_SENSE_NAMES = NO # If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with # their full class and namespace scopes in the documentation. If set to YES, the # scope will be hidden. # The default value is: NO. HIDE_SCOPE_NAMES = NO # If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will # append additional text to a page's title, such as Class Reference. If set to # YES the compound reference will be hidden. # The default value is: NO. HIDE_COMPOUND_REFERENCE= NO # If the SHOW_HEADERFILE tag is set to YES then the documentation for a class # will show which file needs to be included to use the class. # The default value is: YES. SHOW_HEADERFILE = YES # If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of # the files that are included by a file in the documentation of that file. # The default value is: YES. SHOW_INCLUDE_FILES = YES # If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each # grouped member an include statement to the documentation, telling the reader # which file to include in order to use the member. # The default value is: NO. SHOW_GROUPED_MEMB_INC = NO # If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include # files with double quotes in the documentation rather than with sharp brackets. # The default value is: NO. FORCE_LOCAL_INCLUDES = NO # If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the # documentation for inline members. # The default value is: YES. INLINE_INFO = YES # If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the # (detailed) documentation of file and class members alphabetically by member # name. If set to NO, the members will appear in declaration order. # The default value is: YES. SORT_MEMBER_DOCS = YES # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief # descriptions of file, namespace and class members alphabetically by member # name. If set to NO, the members will appear in declaration order. Note that # this will also influence the order of the classes in the class list. # The default value is: NO. SORT_BRIEF_DOCS = NO # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the # (brief and detailed) documentation of class members so that constructors and # destructors are listed first. If set to NO the constructors will appear in the # respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. # Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief # member documentation. # Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting # detailed member documentation. # The default value is: NO. SORT_MEMBERS_CTORS_1ST = NO # If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy # of group names into alphabetical order. If set to NO the group names will # appear in their defined order. # The default value is: NO. SORT_GROUP_NAMES = NO # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by # fully-qualified names, including namespaces. If set to NO, the class list will # be sorted only by class name, not including the namespace part. # Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. # Note: This option applies only to the class list, not to the alphabetical # list. # The default value is: NO. SORT_BY_SCOPE_NAME = NO # If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper # type resolution of all parameters of a function it will reject a match between # the prototype and the implementation of a member function even if there is # only one candidate or it is obvious which candidate to choose by doing a # simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still # accept a match between prototype and implementation in such cases. # The default value is: NO. STRICT_PROTO_MATCHING = NO # The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo # list. This list is created by putting \todo commands in the documentation. # The default value is: YES. GENERATE_TODOLIST = YES # The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test # list. This list is created by putting \test commands in the documentation. # The default value is: YES. GENERATE_TESTLIST = YES # The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug # list. This list is created by putting \bug commands in the documentation. # The default value is: YES. GENERATE_BUGLIST = YES # The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) # the deprecated list. This list is created by putting \deprecated commands in # the documentation. # The default value is: YES. GENERATE_DEPRECATEDLIST= YES # The ENABLED_SECTIONS tag can be used to enable conditional documentation # sections, marked by \if ... \endif and \cond # ... \endcond blocks. ENABLED_SECTIONS = # The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the # initial value of a variable or macro / define can have for it to appear in the # documentation. If the initializer consists of more lines than specified here # it will be hidden. Use a value of 0 to hide initializers completely. The # appearance of the value of individual variables and macros / defines can be # controlled using \showinitializer or \hideinitializer command in the # documentation regardless of this setting. # Minimum value: 0, maximum value: 10000, default value: 30. MAX_INITIALIZER_LINES = 30 # Set the SHOW_USED_FILES tag to NO to disable the list of files generated at # the bottom of the documentation of classes and structs. If set to YES, the # list will mention the files that were used to generate the documentation. # The default value is: YES. SHOW_USED_FILES = YES # Set the SHOW_FILES tag to NO to disable the generation of the Files page. This # will remove the Files entry from the Quick Index and from the Folder Tree View # (if specified). # The default value is: YES. SHOW_FILES = YES # Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces # page. This will remove the Namespaces entry from the Quick Index and from the # Folder Tree View (if specified). # The default value is: YES. SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via # popen()) the command command input-file, where command is the value of the # FILE_VERSION_FILTER tag, and input-file is the name of an input file provided # by doxygen. Whatever the program writes to standard output is used as the file # version. For an example see the documentation. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed # by doxygen. The layout file controls the global structure of the generated # output files in an output format independent way. To create the layout file # that represents doxygen's defaults, run doxygen with the -l option. You can # optionally specify a file name after the option, if omitted DoxygenLayout.xml # will be used as the name of the layout file. See also section "Changing the # layout of pages" for information. # # Note that if you run doxygen from a directory containing a file called # DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE # tag is left empty. LAYOUT_FILE = # The CITE_BIB_FILES tag can be used to specify one or more bib files containing # the reference definitions. This must be a list of .bib files. The .bib # extension is automatically appended if omitted. This requires the bibtex tool # to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. # For LaTeX the style of the bibliography can be controlled using # LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the # search path. See also \cite for info how to create references. CITE_BIB_FILES = #--------------------------------------------------------------------------- # Configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated to # standard output by doxygen. If QUIET is set to YES this implies that the # messages are off. # The default value is: NO. QUIET = NO # The WARNINGS tag can be used to turn on/off the warning messages that are # generated to standard error (stderr) by doxygen. If WARNINGS is set to YES # this implies that the warnings are on. # # Tip: Turn warnings on while writing the documentation. # The default value is: YES. WARNINGS = YES # If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate # warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag # will automatically be disabled. # The default value is: YES. WARN_IF_UNDOCUMENTED = YES # If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as documenting some parameters in # a documented function twice, or documenting parameters that don't exist or # using markup commands wrongly. # The default value is: YES. WARN_IF_DOC_ERROR = YES # If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete # function parameter documentation. If set to NO, doxygen will accept that some # parameters have no documentation without warning. # The default value is: YES. WARN_IF_INCOMPLETE_DOC = YES # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that # are documented, but have no documentation for their parameters or return # value. If set to NO, doxygen will only warn about wrong parameter # documentation, but not about the absence of documentation. If EXTRACT_ALL is # set to YES then this flag will automatically be disabled. See also # WARN_IF_INCOMPLETE_DOC # The default value is: NO. WARN_NO_PARAMDOC = NO # If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS # then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but # at the end of the doxygen process doxygen will return with a non-zero status. # Possible values are: NO, YES and FAIL_ON_WARNINGS. # The default value is: NO. WARN_AS_ERROR = NO # The WARN_FORMAT tag determines the format of the warning messages that doxygen # can produce. The string should contain the $file, $line, and $text tags, which # will be replaced by the file and line number from which the warning originated # and the warning text. Optionally the format may contain $version, which will # be replaced by the version of the file (if it could be obtained via # FILE_VERSION_FILTER) # The default value is: $file:$line: $text. WARN_FORMAT = "$file:$line: $text" # The WARN_LOGFILE tag can be used to specify a file to which warning and error # messages should be written. If left blank the output is written to standard # error (stderr). WARN_LOGFILE = #--------------------------------------------------------------------------- # Configuration options related to the input files #--------------------------------------------------------------------------- # The INPUT tag is used to specify the files and/or directories that contain # documented source files. You may enter file names like myfile.cpp or # directories like /usr/src/myproject. Separate the files or directories with # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. INPUT = ../Nakama/ ../Satori ../README.md ../CHANGELOG.md # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses # libiconv (or the iconv built into libc) for the transcoding. See the libiconv # documentation (see: # https://www.gnu.org/software/libiconv/) for the list of possible encodings. # The default value is: UTF-8. INPUT_ENCODING = UTF-8 # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and # *.h) to filter out the source-files in the directories. # # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not # read by doxygen. # # Note the list of default checked file patterns might differ from the list of # default file extension mappings. # # If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, # *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, # *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, # *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C # comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, # *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.c \ *.cc \ *.cxx \ *.cpp \ *.c++ \ *.java \ *.ii \ *.ixx \ *.ipp \ *.i++ \ *.inl \ *.idl \ *.ddl \ *.odl \ *.h \ *.hh \ *.hxx \ *.hpp \ *.h++ \ *.l \ *.cs \ *.d \ *.php \ *.php4 \ *.php5 \ *.phtml \ *.inc \ *.m \ *.markdown \ *.md \ *.mm \ *.dox \ *.py \ *.pyw \ *.f90 \ *.f95 \ *.f03 \ *.f08 \ *.f18 \ *.f \ *.for \ *.vhd \ *.vhdl \ *.ucf \ *.qsf \ *.ice # The RECURSIVE tag can be used to specify whether or not subdirectories should # be searched for input files as well. # The default value is: NO. RECURSIVE = YES # The EXCLUDE tag can be used to specify files and/or directories that should be # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. # # Note that relative paths are relative to the directory from which doxygen is # run. EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded # from the input. # The default value is: NO. EXCLUDE_SYMLINKS = NO # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude # certain files from those directories. # # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories for example use the pattern */test/* EXCLUDE_PATTERNS = # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # AClass::ANamespace, ANamespace::*Test # # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories use the pattern */test/* EXCLUDE_SYMBOLS = # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include # command). EXAMPLE_PATH = # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and # *.h) to filter out the source-files in the directories. If left blank all # files are included. EXAMPLE_PATTERNS = * # If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be # searched for input files to be used with the \include or \dontinclude commands # irrespective of the value of the RECURSIVE tag. # The default value is: NO. EXAMPLE_RECURSIVE = NO # The IMAGE_PATH tag can be used to specify one or more files or directories # that contain images that are to be included in the documentation (see the # \image command). IMAGE_PATH = # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command: # # # # where is the value of the INPUT_FILTER tag, and is the # name of an input file. Doxygen will then use the output that the filter # program writes to standard output. If FILTER_PATTERNS is specified, this tag # will be ignored. # # Note that the filter must not add or remove lines; it is applied before the # code is scanned, but not when the output code is generated. If lines are added # or removed, the anchors will not be placed correctly. # # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not # properly processed by doxygen. INPUT_FILTER = # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. Doxygen will compare the file name with each pattern and apply the # filter if there is a match. The filters are a list of the form: pattern=filter # (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how # filters are used. If the FILTER_PATTERNS tag is empty or if none of the # patterns match the file name, INPUT_FILTER is applied. # # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not # properly processed by doxygen. FILTER_PATTERNS = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # INPUT_FILTER) will also be used to filter the input files that are used for # producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). # The default value is: NO. FILTER_SOURCE_FILES = NO # The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file # pattern. A pattern will override the setting for FILTER_PATTERN (if any) and # it is also possible to disable source filtering for a specific pattern using # *.ext= (so without naming a filter). # This tag requires that the tag FILTER_SOURCE_FILES is set to YES. FILTER_SOURCE_PATTERNS = # If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that # is part of the input, its contents will be placed on the main page # (index.html). This can be useful if you have a project on for instance GitHub # and want to reuse the introduction page also for the doxygen output. USE_MDFILE_AS_MAINPAGE = README.md #--------------------------------------------------------------------------- # Configuration options related to source browsing #--------------------------------------------------------------------------- # If the SOURCE_BROWSER tag is set to YES then a list of source files will be # generated. Documented entities will be cross-referenced with these sources. # # Note: To get rid of all source code in the generated output, make sure that # also VERBATIM_HEADERS is set to NO. # The default value is: NO. SOURCE_BROWSER = NO # Setting the INLINE_SOURCES tag to YES will include the body of functions, # classes and enums directly into the documentation. # The default value is: NO. INLINE_SOURCES = NO # Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any # special comment blocks from generated source code fragments. Normal C, C++ and # Fortran comments will always remain visible. # The default value is: YES. STRIP_CODE_COMMENTS = YES # If the REFERENCED_BY_RELATION tag is set to YES then for each documented # entity all documented functions referencing it will be listed. # The default value is: NO. REFERENCED_BY_RELATION = NO # If the REFERENCES_RELATION tag is set to YES then for each documented function # all documented entities called/used by that function will be listed. # The default value is: NO. REFERENCES_RELATION = NO # If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set # to YES then the hyperlinks from functions in REFERENCES_RELATION and # REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will # link to the documentation. # The default value is: YES. REFERENCES_LINK_SOURCE = YES # If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the # source code will show a tooltip with additional information such as prototype, # brief description and links to the definition and documentation. Since this # will make the HTML file larger and loading of large files a bit slower, you # can opt to disable this feature. # The default value is: YES. # This tag requires that the tag SOURCE_BROWSER is set to YES. SOURCE_TOOLTIPS = YES # If the USE_HTAGS tag is set to YES then the references to source code will # point to the HTML generated by the htags(1) tool instead of doxygen built-in # source browser. The htags tool is part of GNU's global source tagging system # (see https://www.gnu.org/software/global/global.html). You will need version # 4.8.6 or higher. # # To use it do the following: # - Install the latest version of global # - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file # - Make sure the INPUT points to the root of the source tree # - Run doxygen as normal # # Doxygen will invoke htags (and that will in turn invoke gtags), so these # tools must be available from the command line (i.e. in the search path). # # The result: instead of the source browser generated by doxygen, the links to # source code will now point to the output of htags. # The default value is: NO. # This tag requires that the tag SOURCE_BROWSER is set to YES. USE_HTAGS = NO # If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a # verbatim copy of the header file for each class for which an include is # specified. Set to NO to disable this. # See also: Section \class. # The default value is: YES. VERBATIM_HEADERS = YES #--------------------------------------------------------------------------- # Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- # If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all # compounds will be generated. Enable this if the project contains a lot of # classes, structs, unions or interfaces. # The default value is: YES. ALPHABETICAL_INDEX = YES # In case all classes in a project start with a common prefix, all classes will # be put under the same header in the alphabetical index. The IGNORE_PREFIX tag # can be used to specify a prefix (or a list of prefixes) that should be ignored # while generating the index headers. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES. IGNORE_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the HTML output #--------------------------------------------------------------------------- # If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output # The default value is: YES. GENERATE_HTML = YES # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of # it. # The default directory is: html. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_OUTPUT = html # The HTML_FILE_EXTENSION tag can be used to specify the file extension for each # generated HTML page (for example: .htm, .php, .asp). # The default value is: .html. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a user-defined HTML header file for # each generated HTML page. If the tag is left blank doxygen will generate a # standard header. # # To get valid HTML the header file that includes any scripts and style sheets # that doxygen needs, which is dependent on the configuration options used (e.g. # the setting GENERATE_TREEVIEW). It is highly recommended to start with a # default header using # doxygen -w html new_header.html new_footer.html new_stylesheet.css # YourConfigFile # and then modify the file new_header.html. See also section "Doxygen usage" # for information on how to generate the default header that doxygen normally # uses. # Note: The header is subject to change so you typically have to regenerate the # default header when upgrading to a newer version of doxygen. For a description # of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_HEADER = # The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each # generated HTML page. If the tag is left blank doxygen will generate a standard # footer. See HTML_HEADER for more information on how to generate a default # footer and what special commands can be used inside the footer. See also # section "Doxygen usage" for information on how to generate the default footer # that doxygen normally uses. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading style # sheet that is used by each HTML page. It can be used to fine-tune the look of # the HTML output. If left blank doxygen will generate a default style sheet. # See also section "Doxygen usage" for information on how to generate the style # sheet that doxygen normally uses. # Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as # it is more robust and this tag (HTML_STYLESHEET) will in the future become # obsolete. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_STYLESHEET = # The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined # cascading style sheets that are included after the standard style sheets # created by doxygen. Using this option one can overrule certain style aspects. # This is preferred over using HTML_STYLESHEET since it does not replace the # standard style sheet and is therefore more robust against future updates. # Doxygen will copy the style sheet files to the output directory. # Note: The order of the extra style sheet files is of importance (e.g. the last # style sheet in the list overrules the setting of the previous ones in the # list). For an example see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_EXTRA_STYLESHEET = # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note # that these files will be copied to the base HTML output directory. Use the # $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these # files. In the HTML_STYLESHEET file, use the file name only. Also note that the # files will be copied as-is; there are no commands or markers available. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_EXTRA_FILES = # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen # will adjust the colors in the style sheet and background images according to # this color. Hue is specified as an angle on a color-wheel, see # https://en.wikipedia.org/wiki/Hue for more information. For instance the value # 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 # purple, and 360 is red again. # Minimum value: 0, maximum value: 359, default value: 220. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_COLORSTYLE_HUE = 220 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors # in the HTML output. For a value of 0 the output will use gray-scales only. A # value of 255 will produce the most vivid colors. # Minimum value: 0, maximum value: 255, default value: 100. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_COLORSTYLE_SAT = 100 # The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the # luminance component of the colors in the HTML output. Values below 100 # gradually make the output lighter, whereas values above 100 make the output # darker. The value divided by 100 is the actual gamma applied, so 80 represents # a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not # change the gamma. # Minimum value: 40, maximum value: 240, default value: 80. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_COLORSTYLE_GAMMA = 80 # If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML # page will contain the date and time when the page was generated. Setting this # to YES can help to show when doxygen was last run and thus if the # documentation is up to date. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_TIMESTAMP = NO # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that # are dynamically created via JavaScript. If disabled, the navigation index will # consists of multiple levels of tabs that are statically embedded in every HTML # page. Disable this option to support browsers that do not have JavaScript, # like the Qt help browser. # The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_DYNAMIC_MENUS = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the # page has loaded. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_DYNAMIC_SECTIONS = NO # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to # such a level that at most the specified number of entries are visible (unless # a fully collapsed tree already exceeds this amount). So setting the number of # entries 1 will produce a full collapsed tree by default. 0 is a special value # representing an infinite number of entries and will result in a full expanded # tree by default. # Minimum value: 0, maximum value: 9999, default value: 100. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_INDEX_NUM_ENTRIES = 100 # If the GENERATE_DOCSET tag is set to YES, additional index files will be # generated that can be used as input for Apple's Xcode 3 integrated development # environment (see: # https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To # create a documentation set, doxygen will generate a Makefile in the HTML # output directory. Running make will produce the docset in that directory and # running make install will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at # startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy # genXcode/_index.html for more information. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_DOCSET = NO # This tag determines the name of the docset feed. A documentation feed provides # an umbrella under which multiple documentation sets from a single provider # (such as a company or product suite) can be grouped. # The default value is: Doxygen generated docs. # This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_FEEDNAME = "Doxygen generated docs" # This tag specifies a string that should uniquely identify the documentation # set bundle. This should be a reverse domain-name style string, e.g. # com.mycompany.MyDocSet. Doxygen will append .docset to the name. # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_BUNDLE_ID = org.doxygen.Project # The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify # the documentation publisher. This should be a reverse domain-name style # string, e.g. com.mycompany.MyDocSet.documentation. # The default value is: org.doxygen.Publisher. # This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_PUBLISHER_ID = org.doxygen.Publisher # The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. # The default value is: Publisher. # This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_PUBLISHER_NAME = Publisher # If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three # additional HTML index files: index.hhp, index.hhc, and index.hhk. The # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop # on Windows. In the beginning of 2021 Microsoft took the original page, with # a.o. the download links, offline the HTML help workshop was already many years # in maintenance mode). You can download the HTML help workshop from the web # archives at Installation executable (see: # http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo # ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). # # The HTML Help Workshop contains a compiler that can convert all HTML output # generated by doxygen into a single compiled HTML file (.chm). Compiled HTML # files are now used as the Windows 98 help format, and will replace the old # Windows help format (.hlp) on all Windows platforms in the future. Compressed # HTML files also contain an index, a table of contents, and you can search for # words in the documentation. The HTML workshop also contains a viewer for # compressed HTML files. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_HTMLHELP = NO # The CHM_FILE tag can be used to specify the file name of the resulting .chm # file. You can add a path in front of the file if the result should not be # written to the html output directory. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. CHM_FILE = # The HHC_LOCATION tag can be used to specify the location (absolute path # including file name) of the HTML help compiler (hhc.exe). If non-empty, # doxygen will try to run the HTML help compiler on the generated index.hhp. # The file has to be specified with full path. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. HHC_LOCATION = # The GENERATE_CHI flag controls if a separate .chi index file is generated # (YES) or that it should be included in the main .chm file (NO). # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. GENERATE_CHI = NO # The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) # and project file content. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. CHM_INDEX_ENCODING = # The BINARY_TOC flag controls whether a binary table of contents is generated # (YES) or a normal table of contents (NO) in the .chm file. Furthermore it # enables the Previous and Next buttons. # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. BINARY_TOC = NO # The TOC_EXPAND flag can be set to YES to add extra items for group members to # the table of contents of the HTML help documentation and to the tree view. # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. TOC_EXPAND = NO # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help # (.qch) of the generated HTML documentation. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_QHP = NO # If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify # the file name of the resulting .qch file. The path specified is relative to # the HTML output folder. # This tag requires that the tag GENERATE_QHP is set to YES. QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help # Project output. For more information please see Qt Help Project / Namespace # (see: # https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_QHP is set to YES. QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt # Help Project output. For more information please see Qt Help Project / Virtual # Folders (see: # https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). # The default value is: doc. # This tag requires that the tag GENERATE_QHP is set to YES. QHP_VIRTUAL_FOLDER = doc # If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom # filter to add. For more information please see Qt Help Project / Custom # Filters (see: # https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_NAME = # The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the # custom filter to add. For more information please see Qt Help Project / Custom # Filters (see: # https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this # project's filter section matches. Qt Help Project / Filter Attributes (see: # https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_SECT_FILTER_ATTRS = # The QHG_LOCATION tag can be used to specify the location (absolute path # including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to # run qhelpgenerator on the generated .qhp file. # This tag requires that the tag GENERATE_QHP is set to YES. QHG_LOCATION = # If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be # generated, together with the HTML files, they form an Eclipse help plugin. To # install this plugin and make it available under the help contents menu in # Eclipse, the contents of the directory containing the HTML and XML files needs # to be copied into the plugins directory of eclipse. The name of the directory # within the plugins directory should be the same as the ECLIPSE_DOC_ID value. # After copying Eclipse needs to be restarted before the help appears. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_ECLIPSEHELP = NO # A unique identifier for the Eclipse help plugin. When installing the plugin # the directory name containing the HTML and XML files should also have this # name. Each documentation set should have its own identifier. # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. ECLIPSE_DOC_ID = org.doxygen.Project # If you want full control over the layout of the generated HTML pages it might # be necessary to disable the index and replace it with your own. The # DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top # of each HTML page. A value of NO enables the index and the value YES disables # it. Since the tabs in the index contain the same information as the navigation # tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. DISABLE_INDEX = NO # The GENERATE_TREEVIEW tag is used to specify whether a tree-like index # structure should be generated to display hierarchical information. If the tag # value is set to YES, a side panel will be generated containing a tree-like # index structure (just like the one that is generated for HTML Help). For this # to work a browser that supports JavaScript, DHTML, CSS and frames is required # (i.e. any modern browser). Windows users are probably better off using the # HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can # further fine tune the look of the index (see "Fine-tuning the output"). As an # example, the default style sheet generated by doxygen has an example that # shows how to put an image at the root of the tree instead of the PROJECT_NAME. # Since the tree basically has the same information as the tab index, you could # consider setting DISABLE_INDEX to YES when enabling this option. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_TREEVIEW = YES # When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the # FULL_SIDEBAR option determines if the side bar is limited to only the treeview # area (value NO) or if it should extend to the full height of the window (value # YES). Setting this to YES gives a layout similar to # https://docs.readthedocs.io with more room for contents, but less room for the # project logo, title, and description. If either GENERATOR_TREEVIEW or # DISABLE_INDEX is set to NO, this option has no effect. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. FULL_SIDEBAR = NO # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that # doxygen will group on one line in the generated HTML documentation. # # Note that a value of 0 will completely suppress the enum values from appearing # in the overview section. # Minimum value: 0, maximum value: 20, default value: 4. # This tag requires that the tag GENERATE_HTML is set to YES. ENUM_VALUES_PER_LINE = 4 # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used # to set the initial width (in pixels) of the frame in which the tree is shown. # Minimum value: 0, maximum value: 1500, default value: 250. # This tag requires that the tag GENERATE_HTML is set to YES. TREEVIEW_WIDTH = 250 # If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to # external symbols imported via tag files in a separate window. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. EXT_LINKS_IN_WINDOW = NO # If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg # tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see # https://inkscape.org) to generate formulas as SVG images instead of PNGs for # the HTML output. These images will generally look nicer at scaled resolutions. # Possible values are: png (the default) and svg (looks nicer but requires the # pdf2svg or inkscape tool). # The default value is: png. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_FORMULA_FORMAT = png # Use this tag to change the font size of LaTeX formulas included as images in # the HTML documentation. When you change the font size after a successful # doxygen run you need to manually remove any form_*.png images from the HTML # output directory to force them to be regenerated. # Minimum value: 8, maximum value: 50, default value: 10. # This tag requires that the tag GENERATE_HTML is set to YES. FORMULA_FONTSIZE = 10 # Use the FORMULA_TRANSPARENT tag to determine whether or not the images # generated for formulas are transparent PNGs. Transparent PNGs are not # supported properly for IE 6.0, but are supported on all modern browsers. # # Note that when changing this option you need to delete any form_*.png files in # the HTML output directory before the changes have effect. # The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. FORMULA_TRANSPARENT = YES # The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands # to create new LaTeX commands to be used in formulas as building blocks. See # the section "Including formulas" for details. FORMULA_MACROFILE = # Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see # https://www.mathjax.org) which uses client side JavaScript for the rendering # instead of using pre-rendered bitmaps. Use this if you do not have LaTeX # installed or if you want to formulas look prettier in the HTML output. When # enabled you may also need to install MathJax separately and configure the path # to it using the MATHJAX_RELPATH option. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. USE_MATHJAX = NO # With MATHJAX_VERSION it is possible to specify the MathJax version to be used. # Note that the different versions of MathJax have different requirements with # regards to the different settings, so it is possible that also other MathJax # settings have to be changed when switching between the different MathJax # versions. # Possible values are: MathJax_2 and MathJax_3. # The default value is: MathJax_2. # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_VERSION = MathJax_2 # When MathJax is enabled you can set the default output format to be used for # the MathJax output. For more details about the output format see MathJax # version 2 (see: # http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 # (see: # http://docs.mathjax.org/en/latest/web/components/output.html). # Possible values are: HTML-CSS (which is slower, but has the best # compatibility. This is the name for Mathjax version 2, for MathJax version 3 # this will be translated into chtml), NativeMML (i.e. MathML. Only supported # for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This # is the name for Mathjax version 3, for MathJax version 2 this will be # translated into HTML-CSS) and SVG. # The default value is: HTML-CSS. # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_FORMAT = HTML-CSS # When MathJax is enabled you need to specify the location relative to the HTML # output directory using the MATHJAX_RELPATH option. The destination directory # should contain the MathJax.js script. For instance, if the mathjax directory # is located at the same level as the HTML output directory, then # MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax # Content Delivery Network so you can quickly see the result without installing # MathJax. However, it is strongly recommended to install a local copy of # MathJax from https://www.mathjax.org before deployment. The default value is: # - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 # - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_RELPATH = # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # extension names that should be enabled during MathJax rendering. For example # for MathJax version 2 (see https://docs.mathjax.org/en/v2.7-latest/tex.html # #tex-and-latex-extensions): # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols # For example for MathJax version 3 (see # http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): # MATHJAX_EXTENSIONS = ams # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_EXTENSIONS = # The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces # of code that will be used on startup of the MathJax code. See the MathJax site # (see: # http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an # example see the documentation. # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_CODEFILE = # When the SEARCHENGINE tag is enabled doxygen will generate a search box for # the HTML output. The underlying search engine uses javascript and DHTML and # should work on any modern browser. Note that when using HTML help # (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) # there is already a search function so this one should typically be disabled. # For large projects the javascript based search engine can be slow, then # enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to # search using the keyboard; to jump to the search box use + S # (what the is depends on the OS and browser, but it is typically # , /