Example #1
0
        public void Parse_Throws_MessageReadException_On_Missing_Data()
        {
            var msg = new MessageBuilder()
                      .WriteCode(MessageCode.Server.NotifyPrivileges)
                      .WriteInteger(1)
                      .Build();

            var ex = Record.Exception(() => PrivilegeNotification.FromByteArray(msg));

            Assert.NotNull(ex);
            Assert.IsType <MessageReadException>(ex);
        }
Example #2
0
        public void Parse_Returns_Expected_Data(int id, string username)
        {
            var builder = new MessageBuilder()
                          .WriteCode(MessageCode.Server.NotifyPrivileges)
                          .WriteInteger(id)
                          .WriteString(username);

            var response = PrivilegeNotification.FromByteArray(builder.Build());

            Assert.Equal(id, response.Id);
            Assert.Equal(username, response.Username);
        }
Example #3
0
        public void Parse_Throws_MessageException_On_Code_Mismatch(int id, string username)
        {
            var msg = new MessageBuilder()
                      .WriteCode(MessageCode.Server.PrivateMessage)
                      .WriteInteger(id)
                      .WriteString(username)
                      .Build();

            var ex = Record.Exception(() => PrivilegeNotification.FromByteArray(msg));

            Assert.NotNull(ex);
            Assert.IsType <MessageException>(ex);
        }
Example #4
0
        /// <summary>
        ///     Handles incoming messages.
        /// </summary>
        /// <param name="sender">The <see cref="IMessageConnection"/> instance from which the message originated.</param>
        /// <param name="message">The message.</param>
        public async void HandleMessageRead(object sender, byte[] message)
        {
            var code = new MessageReader <MessageCode.Server>(message).ReadCode();

            if (code != MessageCode.Server.SearchRequest)
            {
                Diagnostic.Debug($"Server message received: {code}");
            }

            try
            {
                switch (code)
                {
                case MessageCode.Server.ParentMinSpeed:
                case MessageCode.Server.ParentSpeedRatio:
                case MessageCode.Server.WishlistInterval:
                case MessageCode.Server.CheckPrivileges:
                    SoulseekClient.Waiter.Complete(new WaitKey(code), IntegerResponse.FromByteArray <MessageCode.Server>(message));
                    break;

                case MessageCode.Server.NewPassword:
                    var confirmedPassword = NewPassword.FromByteArray(message).Password;
                    SoulseekClient.Waiter.Complete(new WaitKey(code), confirmedPassword);
                    break;

                case MessageCode.Server.GlobalAdminMessage:
                    var msg = GlobalMessageNotification.FromByteArray(message);
                    GlobalMessageReceived?.Invoke(this, new GlobalMessageReceivedEventArgs(msg));
                    break;

                case MessageCode.Server.Ping:
                    SoulseekClient.Waiter.Complete(new WaitKey(code));
                    break;

                case MessageCode.Server.Login:
                    SoulseekClient.Waiter.Complete(new WaitKey(code), LoginResponse.FromByteArray(message));
                    break;

                case MessageCode.Server.RoomList:
                    var roomList = RoomListResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code), roomList);
                    RoomListReceived?.Invoke(this, new RoomListReceivedEventArgs(roomList));
                    break;

                case MessageCode.Server.PrivilegedUsers:
                    var privilegedUserList = PrivilegedUserListNotification.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code), privilegedUserList);
                    PrivilegedUserListReceived?.Invoke(this, new PrivilegedUserListReceivedEventArgs(privilegedUserList));
                    break;

                case MessageCode.Server.AddPrivilegedUser:
                    PrivilegeNotificationReceived?.Invoke(this, new PrivilegeNotificationReceivedEventArgs(PrivilegedUserNotification.FromByteArray(message)));
                    break;

                case MessageCode.Server.NotifyPrivileges:
                    var pn = PrivilegeNotification.FromByteArray(message);
                    PrivilegeNotificationReceived?.Invoke(this, new PrivilegeNotificationReceivedEventArgs(pn.Username, pn.Id));

                    if (SoulseekClient.Options.AutoAcknowledgePrivilegeNotifications)
                    {
                        await SoulseekClient.AcknowledgePrivilegeNotificationAsync(pn.Id, CancellationToken.None).ConfigureAwait(false);
                    }

                    break;

                case MessageCode.Server.UserPrivileges:
                    var privilegeResponse = UserPrivilegeResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, privilegeResponse.Username), privilegeResponse.IsPrivileged);
                    break;

                case MessageCode.Server.NetInfo:
                    var netInfo = NetInfoNotification.FromByteArray(message);

                    try
                    {
                        var parents = netInfo.Parents.Select(parent => (parent.Username, new IPEndPoint(parent.IPAddress, parent.Port)));
                        await SoulseekClient.DistributedConnectionManager.AddParentConnectionAsync(parents).ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        Diagnostic.Debug($"Error handling NetInfo message: {ex.Message}");
                    }

                    break;

                case MessageCode.Server.ConnectToPeer:
                    ConnectToPeerResponse connectToPeerResponse = default;

                    try
                    {
                        connectToPeerResponse = ConnectToPeerResponse.FromByteArray(message);

                        if (connectToPeerResponse.Type == Constants.ConnectionType.Transfer)
                        {
                            Diagnostic.Debug($"Received transfer ConnectToPeer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint}) for remote token {connectToPeerResponse.Token}");

                            // ensure that we are expecting at least one file from this user before we connect. the response
                            // doesn't contain any other identifying information about the file.
                            if (!SoulseekClient.Downloads.IsEmpty && SoulseekClient.Downloads.Values.Any(d => d.Username == connectToPeerResponse.Username))
                            {
                                var(connection, remoteToken) = await SoulseekClient.PeerConnectionManager.GetTransferConnectionAsync(connectToPeerResponse).ConfigureAwait(false);

                                var download = SoulseekClient.Downloads.Values.FirstOrDefault(v => v.RemoteToken == remoteToken && v.Username == connectToPeerResponse.Username);

                                if (download != default(TransferInternal))
                                {
                                    Diagnostic.Debug($"Solicited inbound transfer connection to {download.Username} ({connection.IPEndPoint}) for token {download.Token} (remote: {download.RemoteToken}) established. (id: {connection.Id})");
                                    SoulseekClient.Waiter.Complete(new WaitKey(Constants.WaitKey.IndirectTransfer, download.Username, download.Filename, download.RemoteToken), connection);
                                }
                                else
                                {
                                    Diagnostic.Debug($"Transfer ConnectToPeer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint}) for remote token {connectToPeerResponse.Token} does not match any waiting downloads, discarding.");
                                    connection.Disconnect($"Unknown transfer");
                                }
                            }
                            else
                            {
                                throw new SoulseekClientException($"Unexpected transfer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint}); Ignored");
                            }
                        }
                        else if (connectToPeerResponse.Type == Constants.ConnectionType.Peer)
                        {
                            Diagnostic.Debug($"Received message ConnectToPeer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint})");
                            await SoulseekClient.PeerConnectionManager.GetOrAddMessageConnectionAsync(connectToPeerResponse).ConfigureAwait(false);
                        }
                        else if (connectToPeerResponse.Type == Constants.ConnectionType.Distributed)
                        {
                            Diagnostic.Debug($"Received distributed ConnectToPeer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint})");
                            await SoulseekClient.DistributedConnectionManager.AddChildConnectionAsync(connectToPeerResponse).ConfigureAwait(false);
                        }
                        else
                        {
                            throw new MessageException($"Unknown Connect To Peer connection type '{connectToPeerResponse.Type}'");
                        }
                    }
                    catch (Exception ex)
                    {
                        Diagnostic.Debug($"Error handling ConnectToPeer response from {connectToPeerResponse?.Username} ({connectToPeerResponse?.IPEndPoint}): {ex.Message}");
                    }

                    break;

                case MessageCode.Server.AddUser:
                    var addUserResponse = AddUserResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, addUserResponse.Username), addUserResponse);
                    break;

                case MessageCode.Server.GetStatus:
                    var statsResponse = UserStatusResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, statsResponse.Username), statsResponse);
                    UserStatusChanged?.Invoke(this, new UserStatusChangedEventArgs(statsResponse));
                    break;

                case MessageCode.Server.PrivateMessage:
                    var pm = PrivateMessageNotification.FromByteArray(message);
                    PrivateMessageReceived?.Invoke(this, new PrivateMessageReceivedEventArgs(pm));

                    if (SoulseekClient.Options.AutoAcknowledgePrivateMessages)
                    {
                        await SoulseekClient.AcknowledgePrivateMessageAsync(pm.Id, CancellationToken.None).ConfigureAwait(false);
                    }

                    break;

                case MessageCode.Server.GetPeerAddress:
                    var peerAddressResponse = UserAddressResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, peerAddressResponse.Username), peerAddressResponse);
                    break;

                case MessageCode.Server.JoinRoom:
                    var roomData = RoomJoinResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, roomData.Name), roomData);
                    break;

                case MessageCode.Server.LeaveRoom:
                    var leaveRoomResponse = RoomLeaveResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, leaveRoomResponse.RoomName));
                    break;

                case MessageCode.Server.SayInChatRoom:
                    var roomMessage = RoomMessageNotification.FromByteArray(message);
                    RoomMessageReceived?.Invoke(this, new RoomMessageReceivedEventArgs(roomMessage));
                    break;

                case MessageCode.Server.UserJoinedRoom:
                    var joinNotification = RoomJoinedNotification.FromByteArray(message);
                    RoomJoined?.Invoke(this, new RoomJoinedEventArgs(joinNotification));
                    break;

                case MessageCode.Server.UserLeftRoom:
                    var leftNotification = RoomLeftNotification.FromByteArray(message);
                    RoomLeft?.Invoke(this, new RoomLeftEventArgs(leftNotification));
                    break;

                case MessageCode.Server.KickedFromServer:
                    KickedFromServer?.Invoke(this, EventArgs.Empty);
                    break;

                case MessageCode.Server.FileSearch:
                    var searchRequest = ServerSearchRequest.FromByteArray(message);

                    // sometimes (most of the time?) a room search will result in a request to ourselves (assuming we are
                    // joined to it)
                    if (searchRequest.Username == SoulseekClient.Username)
                    {
                        break;
                    }

                    SearchResponse searchResponse;

                    if (SoulseekClient.Options.SearchResponseResolver == default)
                    {
                        break;
                    }

                    try
                    {
                        searchResponse = await SoulseekClient.Options.SearchResponseResolver(searchRequest.Username, searchRequest.Token, SearchQuery.FromText(searchRequest.Query)).ConfigureAwait(false);

                        if (searchResponse != null && searchResponse.FileCount + searchResponse.LockedFileCount > 0)
                        {
                            var endpoint = await SoulseekClient.GetUserEndPointAsync(searchRequest.Username).ConfigureAwait(false);

                            var peerConnection = await SoulseekClient.PeerConnectionManager.GetOrAddMessageConnectionAsync(searchRequest.Username, endpoint, CancellationToken.None).ConfigureAwait(false);

                            await peerConnection.WriteAsync(searchResponse.ToByteArray()).ConfigureAwait(false);
                        }
                    }
                    catch (Exception ex)
                    {
                        Diagnostic.Warning($"Error resolving search response for query '{searchRequest.Query}' requested by {searchRequest.Username} with token {searchRequest.Token}: {ex.Message}", ex);
                    }

                    break;

                // if we fail to connect to a distributed parent in a timely manner, the server will begin to send us distributed search requests directly.
                // forward these to the distributed message handler.
                case MessageCode.Server.SearchRequest:
                    SoulseekClient.DistributedMessageHandler.HandleMessageRead(SoulseekClient.ServerConnection, message);
                    break;

                default:
                    Diagnostic.Debug($"Unhandled server message: {code}; {message.Length} bytes");
                    break;
                }
            }
            catch (Exception ex)
            {
                Diagnostic.Warning($"Error handling server message: {code}; {ex.Message}", ex);
            }
        }
Example #5
0
        /// <summary>
        ///     Handles incoming messages.
        /// </summary>
        /// <param name="sender">The <see cref="IMessageConnection"/> instance from which the message originated.</param>
        /// <param name="message">The message.</param>
        public async void HandleMessageRead(object sender, byte[] message)
        {
            var code = new MessageReader <MessageCode.Server>(message).ReadCode();

            if (code != MessageCode.Server.EmbeddedMessage)
            {
                Diagnostic.Debug($"Server message received: {code}");
            }

            try
            {
                switch (code)
                {
                case MessageCode.Server.ParentMinSpeed:
                case MessageCode.Server.ParentSpeedRatio:
                case MessageCode.Server.WishlistInterval:
                case MessageCode.Server.CheckPrivileges:
                    SoulseekClient.Waiter.Complete(new WaitKey(code), IntegerResponse.FromByteArray <MessageCode.Server>(message));
                    break;

                case MessageCode.Server.PrivateRoomAdded:
                    PrivateRoomMembershipAdded?.Invoke(this, StringResponse.FromByteArray <MessageCode.Server>(message));
                    break;

                case MessageCode.Server.PrivateRoomRemoved:
                    var privateRoomRemoved = StringResponse.FromByteArray <MessageCode.Server>(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, privateRoomRemoved));
                    PrivateRoomMembershipRemoved?.Invoke(this, privateRoomRemoved);
                    break;

                case MessageCode.Server.PrivateRoomOperatorAdded:
                    PrivateRoomModerationAdded?.Invoke(this, StringResponse.FromByteArray <MessageCode.Server>(message));
                    break;

                case MessageCode.Server.PrivateRoomOperatorRemoved:
                    var privateRoomOperatorRemoved = StringResponse.FromByteArray <MessageCode.Server>(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, privateRoomOperatorRemoved));
                    PrivateRoomModerationRemoved?.Invoke(this, privateRoomOperatorRemoved);
                    break;

                case MessageCode.Server.NewPassword:
                    var confirmedPassword = NewPassword.FromByteArray(message).Password;
                    SoulseekClient.Waiter.Complete(new WaitKey(code), confirmedPassword);
                    break;

                case MessageCode.Server.PrivateRoomToggle:
                    var acceptInvitations = PrivateRoomToggle.FromByteArray(message).AcceptInvitations;
                    SoulseekClient.Waiter.Complete(new WaitKey(code), acceptInvitations);
                    break;

                case MessageCode.Server.GlobalAdminMessage:
                    var msg = GlobalMessageNotification.FromByteArray(message);
                    GlobalMessageReceived?.Invoke(this, msg);
                    break;

                case MessageCode.Server.Ping:
                    SoulseekClient.Waiter.Complete(new WaitKey(code));
                    break;

                case MessageCode.Server.Login:
                    SoulseekClient.Waiter.Complete(new WaitKey(code), LoginResponse.FromByteArray(message));
                    break;

                case MessageCode.Server.RoomList:
                    var roomList = RoomListResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code), roomList);
                    RoomListReceived?.Invoke(this, roomList);
                    break;

                case MessageCode.Server.PrivateRoomOwned:
                    var moderatedRoomInfo = PrivateRoomOwnedListNotification.FromByteArray(message);
                    PrivateRoomModeratedUserListReceived?.Invoke(this, moderatedRoomInfo);
                    break;

                case MessageCode.Server.PrivateRoomUsers:
                    var roomInfo = PrivateRoomUserListNotification.FromByteArray(message);
                    PrivateRoomUserListReceived?.Invoke(this, roomInfo);
                    break;

                case MessageCode.Server.PrivilegedUsers:
                    var privilegedUserList = PrivilegedUserListNotification.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code), privilegedUserList);
                    PrivilegedUserListReceived?.Invoke(this, privilegedUserList);
                    break;

                case MessageCode.Server.AddPrivilegedUser:
                    PrivilegeNotificationReceived?.Invoke(this, new PrivilegeNotificationReceivedEventArgs(PrivilegedUserNotification.FromByteArray(message)));
                    break;

                case MessageCode.Server.NotifyPrivileges:
                    var pn = PrivilegeNotification.FromByteArray(message);
                    PrivilegeNotificationReceived?.Invoke(this, new PrivilegeNotificationReceivedEventArgs(pn.Username, pn.Id));

                    if (SoulseekClient.Options.AutoAcknowledgePrivilegeNotifications)
                    {
                        await SoulseekClient.AcknowledgePrivilegeNotificationAsync(pn.Id, CancellationToken.None).ConfigureAwait(false);
                    }

                    break;

                case MessageCode.Server.UserPrivileges:
                    var privilegeResponse = UserPrivilegeResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, privilegeResponse.Username), privilegeResponse.IsPrivileged);
                    break;

                case MessageCode.Server.NetInfo:
                    var netInfo = NetInfoNotification.FromByteArray(message);

                    try
                    {
                        var parents = netInfo.Parents.Select(parent => (parent.Username, new IPEndPoint(parent.IPAddress, parent.Port)));
                        await SoulseekClient.DistributedConnectionManager.AddParentConnectionAsync(parents).ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        Diagnostic.Debug($"Error handling NetInfo message: {ex.Message}");
                    }

                    break;

                case MessageCode.Server.CannotConnect:
                    var cannotConnect = CannotConnect.FromByteArray(message);
                    Diagnostic.Debug($"Received CannotConnect message for token {cannotConnect.Token}{(!string.IsNullOrEmpty(cannotConnect.Username) ? $" from user {cannotConnect.Username}" : string.Empty)}");

                    SoulseekClient.SearchResponder.TryDiscard(cannotConnect.Token);

                    if (!string.IsNullOrEmpty(cannotConnect.Username))
                    {
                        UserCannotConnect?.Invoke(this, new UserCannotConnectEventArgs(cannotConnect));
                    }

                    break;

                case MessageCode.Server.CannotJoinRoom:
                    var cannotJoinRoom = CannotJoinRoom.FromByteArray(message);
                    SoulseekClient.Waiter.Throw(
                        new WaitKey(MessageCode.Server.JoinRoom, cannotJoinRoom.RoomName),
                        new RoomJoinForbiddenException($"The server rejected the request to join room {cannotJoinRoom.RoomName}"));

                    break;

                case MessageCode.Server.ConnectToPeer:
                    var connectToPeerResponse = ConnectToPeerResponse.FromByteArray(message);

                    try
                    {
                        if (connectToPeerResponse.Type == Constants.ConnectionType.Transfer)
                        {
                            Diagnostic.Debug($"Received transfer ConnectToPeer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint}) for remote token {connectToPeerResponse.Token}");

                            // ensure that we are expecting at least one file from this user before we connect. the response
                            // doesn't contain any other identifying information about the file.
                            if (!SoulseekClient.Downloads.IsEmpty && SoulseekClient.Downloads.Values.Any(d => d.Username == connectToPeerResponse.Username))
                            {
                                var(connection, remoteToken) = await SoulseekClient.PeerConnectionManager.GetTransferConnectionAsync(connectToPeerResponse).ConfigureAwait(false);

                                var download = SoulseekClient.Downloads.Values.FirstOrDefault(v => v.RemoteToken == remoteToken && v.Username == connectToPeerResponse.Username);

                                if (download != default(TransferInternal))
                                {
                                    Diagnostic.Debug($"Solicited inbound transfer connection to {download.Username} ({connection.IPEndPoint}) for token {download.Token} (remote: {download.RemoteToken}) established. (id: {connection.Id})");
                                    SoulseekClient.Waiter.Complete(new WaitKey(Constants.WaitKey.IndirectTransfer, download.Username, download.Filename, download.RemoteToken), connection);
                                }
                                else
                                {
                                    Diagnostic.Debug($"Transfer ConnectToPeer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint}) for remote token {connectToPeerResponse.Token} does not match any waiting downloads, discarding.");
                                    connection.Disconnect("Unknown transfer");
                                }
                            }
                            else
                            {
                                throw new SoulseekClientException($"Unexpected transfer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint}); Ignored");
                            }
                        }
                        else if (connectToPeerResponse.Type == Constants.ConnectionType.Peer)
                        {
                            Diagnostic.Debug($"Received message ConnectToPeer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint})");
                            await SoulseekClient.PeerConnectionManager.GetOrAddMessageConnectionAsync(connectToPeerResponse).ConfigureAwait(false);
                        }
                        else if (connectToPeerResponse.Type == Constants.ConnectionType.Distributed)
                        {
                            Diagnostic.Debug($"Received distributed ConnectToPeer request from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint})");
                            await SoulseekClient.DistributedConnectionManager.GetOrAddChildConnectionAsync(connectToPeerResponse).ConfigureAwait(false);
                        }
                        else
                        {
                            throw new MessageException($"Unknown Connect To Peer connection type '{connectToPeerResponse.Type}'");
                        }
                    }
                    catch (Exception ex)
                    {
                        Diagnostic.Debug($"Error handling ConnectToPeer response from {connectToPeerResponse.Username} ({connectToPeerResponse.IPEndPoint}): {ex.Message}");
                    }

                    break;

                case MessageCode.Server.AddUser:
                    var addUserResponse = AddUserResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, addUserResponse.Username), addUserResponse);
                    break;

                case MessageCode.Server.GetStatus:
                    var statsResponse = UserStatusResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, statsResponse.Username), statsResponse);
                    UserStatusChanged?.Invoke(this, new UserStatusChangedEventArgs(statsResponse));
                    break;

                case MessageCode.Server.PrivateMessage:
                    var pm = PrivateMessageNotification.FromByteArray(message);
                    PrivateMessageReceived?.Invoke(this, new PrivateMessageReceivedEventArgs(pm));

                    if (SoulseekClient.Options.AutoAcknowledgePrivateMessages)
                    {
                        await SoulseekClient.AcknowledgePrivateMessageAsync(pm.Id, CancellationToken.None).ConfigureAwait(false);
                    }

                    break;

                case MessageCode.Server.GetPeerAddress:
                    var peerAddressResponse = UserAddressResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, peerAddressResponse.Username), peerAddressResponse);
                    break;

                case MessageCode.Server.JoinRoom:
                    var roomData = JoinRoomResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, roomData.Name), roomData);
                    break;

                case MessageCode.Server.LeaveRoom:
                    var leaveRoomResponse = LeaveRoomResponse.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, leaveRoomResponse.RoomName));
                    break;

                case MessageCode.Server.SayInChatRoom:
                    var roomMessage = RoomMessageNotification.FromByteArray(message);
                    RoomMessageReceived?.Invoke(this, new RoomMessageReceivedEventArgs(roomMessage));
                    break;

                case MessageCode.Server.PublicChat:
                    var publicChatMessage = PublicChatMessageNotification.FromByteArray(message);
                    PublicChatMessageReceived?.Invoke(this, new PublicChatMessageReceivedEventArgs(publicChatMessage));
                    break;

                case MessageCode.Server.UserJoinedRoom:
                    var joinNotification = UserJoinedRoomNotification.FromByteArray(message);
                    RoomJoined?.Invoke(this, new RoomJoinedEventArgs(joinNotification));
                    break;

                case MessageCode.Server.UserLeftRoom:
                    var leftNotification = UserLeftRoomNotification.FromByteArray(message);
                    RoomLeft?.Invoke(this, new RoomLeftEventArgs(leftNotification));
                    break;

                case MessageCode.Server.RoomTickers:
                    var roomTickers = RoomTickerListNotification.FromByteArray(message);
                    RoomTickerListReceived?.Invoke(this, new RoomTickerListReceivedEventArgs(roomTickers));
                    break;

                case MessageCode.Server.RoomTickerAdd:
                    var roomTickerAdded = RoomTickerAddedNotification.FromByteArray(message);
                    RoomTickerAdded?.Invoke(this, new RoomTickerAddedEventArgs(roomTickerAdded.RoomName, roomTickerAdded.Ticker));
                    break;

                case MessageCode.Server.RoomTickerRemove:
                    var roomTickerRemoved = RoomTickerRemovedNotification.FromByteArray(message);
                    RoomTickerRemoved?.Invoke(this, new RoomTickerRemovedEventArgs(roomTickerRemoved.RoomName, roomTickerRemoved.Username));
                    break;

                case MessageCode.Server.PrivateRoomAddUser:
                    var privateRoomAddUserResponse = PrivateRoomAddUser.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, privateRoomAddUserResponse.RoomName, privateRoomAddUserResponse.Username));
                    break;

                case MessageCode.Server.PrivateRoomRemoveUser:
                    var privateRoomRemoveUserResponse = PrivateRoomRemoveUser.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, privateRoomRemoveUserResponse.RoomName, privateRoomRemoveUserResponse.Username));
                    break;

                case MessageCode.Server.PrivateRoomAddOperator:
                    var privateRoomAddOperatorResponse = PrivateRoomAddOperator.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, privateRoomAddOperatorResponse.RoomName, privateRoomAddOperatorResponse.Username));
                    break;

                case MessageCode.Server.PrivateRoomRemoveOperator:
                    var privateRoomRemoveOperatorResponse = PrivateRoomRemoveOperator.FromByteArray(message);
                    SoulseekClient.Waiter.Complete(new WaitKey(code, privateRoomRemoveOperatorResponse.RoomName, privateRoomRemoveOperatorResponse.Username));
                    break;

                case MessageCode.Server.KickedFromServer:
                    KickedFromServer?.Invoke(this, EventArgs.Empty);
                    break;

                case MessageCode.Server.FileSearch:
                    var searchRequest = ServerSearchRequest.FromByteArray(message);

                    // sometimes (most of the time?) a room search will result in a request to ourselves (assuming we are
                    // joined to it)
                    if (searchRequest.Username == SoulseekClient.Username)
                    {
                        break;
                    }

                    await SoulseekClient.SearchResponder.TryRespondAsync(searchRequest.Username, searchRequest.Token, searchRequest.Query).ConfigureAwait(false);

                    break;

                case MessageCode.Server.EmbeddedMessage:
                    SoulseekClient.DistributedMessageHandler.HandleEmbeddedMessage(message);
                    break;

                default:
                    Diagnostic.Debug($"Unhandled server message: {code}; {message.Length} bytes");
                    break;
                }
            }
            catch (Exception ex)
            {
                Diagnostic.Warning($"Error handling server message: {code}; {ex.Message}", ex);
            }
        }