/// <summary> /// Adds the specified <paramref name="slimResponse"/> to the list of responses after applying the filters specified in /// the search options. /// </summary> /// <param name="slimResponse">The response to add.</param> internal void AddResponse(SearchResponseSlim slimResponse) { // ensure the search is still active, the token matches and that the response meets basic filtering criteria we check // the slim response for fitness prior to extracting the file list from it for performance reasons. if (State.HasFlag(SearchStates.InProgress) && slimResponse.Token == Token && SlimResponseMeetsOptionCriteria(slimResponse)) { // extract the file list from the response and filter it var fullResponse = new SearchResponse(slimResponse); var filteredFiles = fullResponse.Files.Where(f => Options.FileFilter?.Invoke(f) ?? true); fullResponse = new SearchResponse(fullResponse, filteredFiles); // ensure the filtered file count still meets the response criteria if ((Options.FilterResponses && fullResponse.FileCount < Options.MinimumResponseFileCount) || !(Options.ResponseFilter?.Invoke(fullResponse) ?? true)) { return; } Interlocked.Increment(ref resultCount); Interlocked.Add(ref resultFileCount, fullResponse.Files.Count); ResponseBag.Add(fullResponse); ResponseReceived?.Invoke(fullResponse); SearchTimeoutTimer.Reset(); if (resultCount >= Options.ResponseLimit) { Complete(SearchStates.ResponseLimitReached); } else if (resultFileCount >= Options.FileLimit) { Complete(SearchStates.FileLimitReached); } } }
/// <summary> /// Initializes a new instance of the <see cref="SearchResponse"/> class. /// </summary> /// <param name="searchResponse">An existing instance from which to copy properties.</param> /// <param name="fileList">The file list with which to replace the existing file list.</param> /// <param name="lockedFileList">The optional locked file list with which to replace the existing locked file list.</param> internal SearchResponse(SearchResponse searchResponse, IEnumerable <File> fileList, IEnumerable <File> lockedFileList = null) : this(searchResponse.Username, searchResponse.Token, searchResponse.FreeUploadSlots, searchResponse.UploadSpeed, searchResponse.QueueLength, fileList, lockedFileList) { }
/// <summary> /// Initializes a new instance of the <see cref="SearchResponseReceivedEventArgs"/> class. /// </summary> /// <param name="response">The search response which raised the event.</param> /// <param name="search">The search instance with which to initialize data.</param> internal SearchResponseReceivedEventArgs(SearchResponse response, Search search) : base(search) { Response = response; }
/// <summary> /// Responds to the given search request, if a response could be resolved and matche(s) were found. /// </summary> /// <param name="username">The username of the requesting user.</param> /// <param name="token">The token for the search request.</param> /// <param name="query">The search query.</param> /// <returns>The operation context, including a value indicating whether a response was successfully sent.</returns> public async Task <bool> TryRespondAsync(string username, int token, string query) { RequestReceived?.Invoke(this, new SearchRequestEventArgs(username, token, query)); if (SoulseekClient.Options.SearchResponseResolver == default) { return(false); } SearchResponse searchResponse = null; try { searchResponse = await SoulseekClient.Options.SearchResponseResolver(username, token, SearchQuery.FromText(query)).ConfigureAwait(false); } catch (Exception ex) { Diagnostic.Warning($"Error resolving search response for query '{query}' requested by {username} with token {token}: {ex.Message}", ex); return(false); } if (searchResponse == null || searchResponse.FileCount + searchResponse.LockedFileCount <= 0) { return(false); } try { Diagnostic.Debug($"Resolved {searchResponse.FileCount} files for query '{query}' with token {token} from {username}"); var endpoint = await SoulseekClient.GetUserEndPointAsync(username).ConfigureAwait(false); var responseToken = SoulseekClient.GetNextToken(); IMessageConnection peerConnection = default; try { // attempt to connect and send the results immediately. either a direct connection succeeds, or a user // responds to a solicited connection request prior to the configured connection timeout. peerConnection = await SoulseekClient.PeerConnectionManager.GetOrAddMessageConnectionAsync(username, endpoint, solicitationToken : responseToken, CancellationToken.None).ConfigureAwait(false); } catch { // direct connection failed, and user did not respond to the solicited connection request before the timeout, // but may respond later. cache the result along with the solicitation token that was sent so we can attempt a // "second chance" delivery of results if (SoulseekClient.Options.SearchResponseCache != default) { try { SoulseekClient.Options.SearchResponseCache.AddOrUpdate(responseToken, (username, token, query, searchResponse)); Diagnostic.Debug($"Failed to connect to {username} with solicitation token {responseToken} to deliver search results for query '{query}' with token {token}. Cached response for potential delayed delivery."); } catch (Exception ex) { Diagnostic.Warning($"Error caching undelivered search response {responseToken} for query '{query}' requested by {username} with token {token}: {ex.Message}", ex); } } throw; } await peerConnection.WriteAsync(searchResponse.ToByteArray()).ConfigureAwait(false); Diagnostic.Debug($"Sent response containing {searchResponse.FileCount + searchResponse.LockedFileCount} files to {username} for query '{query}' with token {token}"); ResponseDelivered?.Invoke(this, new SearchRequestResponseEventArgs(username, token, query, searchResponse)); return(true); } catch (Exception ex) { Diagnostic.Debug($"Failed to send search response to {username} for query '{query}' with token {token}: {ex.Message}", ex); } return(false); }