public async Task Client_Connect_Raises_StateChanged_Event() { using (var client = new SoulseekClient()) { var events = new List <SoulseekClientStateChangedEventArgs>(); client.StateChanged += (sender, e) => events.Add(e); var ex = await Record.ExceptionAsync(() => client.ConnectAsync(Settings.Username, Settings.Password)); Assert.Null(ex); Assert.Equal(4, events.Count); Assert.Equal(SoulseekClientStates.Connecting, events[0].State); Assert.Equal(SoulseekClientStates.Connected, events[1].State); Assert.Equal(SoulseekClientStates.Connected | SoulseekClientStates.LoggingIn, events[2].State); Assert.Equal(SoulseekClientStates.Connected | SoulseekClientStates.LoggedIn, events[3].State); } }
public async Task Disconnect_Clears_Searches() { var c = new Mock <IMessageConnection>(); var s = new SoulseekClient(Guid.NewGuid().ToString(), new Random().Next(), serverConnection: c.Object); await s.ConnectAsync(); var searches = new ConcurrentDictionary <int, Search>(); searches.TryAdd(0, new Search(string.Empty, 0, new SearchOptions())); searches.TryAdd(1, new Search(string.Empty, 1, new SearchOptions())); s.SetProperty("ActiveSearches", searches); var ex = Record.Exception(() => s.Disconnect()); Assert.Null(ex); Assert.Equal(SoulseekClientStates.Disconnected, s.State); Assert.Empty(searches); }
public async Task Disconnect_Clears_Downloads() { var c = new Mock <IMessageConnection>(); var s = new SoulseekClient(Guid.NewGuid().ToString(), new Random().Next(), serverConnection: c.Object); await s.ConnectAsync(); var downloads = new ConcurrentDictionary <int, Download>(); downloads.TryAdd(0, new Download(string.Empty, string.Empty, 0)); downloads.TryAdd(1, new Download(string.Empty, string.Empty, 1)); s.SetProperty("Downloads", downloads); var ex = Record.Exception(() => s.Disconnect()); Assert.Null(ex); Assert.Equal(SoulseekClientStates.Disconnected, s.State); Assert.Empty(downloads); }
public async Task Connect_Address_Succeeds_When_TcpConnection_Succeeds(IPEndPoint endpoint) { var c = new Mock <IMessageConnection>(); var factory = new Mock <IConnectionFactory>(); factory.Setup(m => m.GetServerConnection( It.IsAny <IPEndPoint>(), It.IsAny <EventHandler>(), It.IsAny <EventHandler <ConnectionDisconnectedEventArgs> >(), It.IsAny <EventHandler <MessageReadEventArgs> >(), It.IsAny <ConnectionOptions>(), It.IsAny <ITcpClient>())) .Returns(c.Object); using (var s = new SoulseekClient(connectionFactory: factory.Object)) { var ex = await Record.ExceptionAsync(async() => await s.ConnectAsync(endpoint.Address.ToString(), endpoint.Port)); Assert.Null(ex); } }
public async Task Address_Throws_ListenPortException_On_Bad_Listen_Port() { var port = Mocks.Port; using (var s = new SoulseekClient(new SoulseekClientOptions(enableListener: true, listenPort: port))) { Listener listener = null; try { listener = new Listener(port, new ConnectionOptions()); listener.Start(); var ex = await Record.ExceptionAsync(() => s.ConnectAsync("u", "p")); Assert.NotNull(ex); Assert.IsType <ListenPortException>(ex); } finally { listener.Stop(); } } }
public void Configure( IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider, ITransferTracker tracker, IBrowseTracker browseTracker, IConversationTracker conversationTracker, IRoomTracker roomTracker) { if (!env.IsDevelopment()) { app.UseHsts(); } app.UseCors("AllowAll"); BasePath = BasePath ?? "/"; BasePath = BasePath.StartsWith("/") ? BasePath : $"/{BasePath}"; app.UsePathBase(BasePath); // remove any errant double forward slashes which may have been introduced // by a reverse proxy or having the base path removed app.Use(async(context, next) => { var path = context.Request.Path.ToString(); if (path.StartsWith("//")) { context.Request.Path = new string(path.Skip(1).ToArray()); } await next(); }); WebRoot = WebRoot ?? Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase).AbsolutePath), "wwwroot"); Console.WriteLine($"Serving static content from {WebRoot}"); var fileServerOptions = new FileServerOptions { FileProvider = new PhysicalFileProvider(WebRoot), RequestPath = "", EnableDirectoryBrowsing = false, EnableDefaultFiles = true }; app.UseFileServer(fileServerOptions); app.UseAuthentication(); app.UseMvc(); app.UseSwagger(); app.UseSwaggerUI(options => provider.ApiVersionDescriptions.ToList() .ForEach(description => options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName))); // if we made it this far and the route still wasn't matched, return the index // this is required so that SPA routing (React Router, etc) can work properly app.Use(async(context, next) => { // exclude API routes which are not matched or return a 404 if (!context.Request.Path.StartsWithSegments("/api")) { context.Request.Path = "/"; } await next(); }); app.UseFileServer(fileServerOptions); // --------------------------------------------------------------------------------------------------------------------------------------------- // begin SoulseekClient implementation // --------------------------------------------------------------------------------------------------------------------------------------------- var connectionOptions = new ConnectionOptions( readBufferSize: ReadBufferSize, writeBufferSize: WriteBufferSize, connectTimeout: ConnectTimeout, inactivityTimeout: InactivityTimeout); // create options for the client. // see the implementation of Func<> and Action<> options for detailed info. var clientOptions = new SoulseekClientOptions( listenPort: ListenPort, userEndPointCache: new UserEndPointCache(), distributedChildLimit: DistributedChildLimit, enableDistributedNetwork: EnableDistributedNetwork, minimumDiagnosticLevel: DiagnosticLevel, autoAcknowledgePrivateMessages: false, serverConnectionOptions: connectionOptions, peerConnectionOptions: connectionOptions, transferConnectionOptions: connectionOptions, userInfoResponseResolver: UserInfoResponseResolver, browseResponseResolver: BrowseResponseResolver, directoryContentsResponseResolver: DirectoryContentsResponseResolver, enqueueDownloadAction: (username, endpoint, filename) => EnqueueDownloadAction(username, endpoint, filename, tracker), searchResponseResolver: SearchResponseResolver); Client = new SoulseekClient(options: clientOptions); // bind the DiagnosticGenerated event so we can trap and display diagnostic messages. this is optional, and if the event // isn't bound the minimumDiagnosticLevel should be set to None. Client.DiagnosticGenerated += (e, args) => { lock (ConsoleSyncRoot) { if (args.Level == DiagnosticLevel.Debug) { Console.ForegroundColor = ConsoleColor.DarkGray; } if (args.Level == DiagnosticLevel.Warning) { Console.ForegroundColor = ConsoleColor.Yellow; } Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [DIAGNOSTIC:{e.GetType().Name}] [{args.Level}] {args.Message}"); Console.ResetColor(); } }; // bind transfer events. see TransferStateChangedEventArgs and TransferProgressEventArgs. Client.TransferStateChanged += (e, args) => { var direction = args.Transfer.Direction.ToString().ToUpper(); var user = args.Transfer.Username; var file = Path.GetFileName(args.Transfer.Filename); var oldState = args.PreviousState; var state = args.Transfer.State; var completed = args.Transfer.State.HasFlag(TransferStates.Completed); Console.WriteLine($"[{direction}] [{user}/{file}] {oldState} => {state}{(completed ? $" ({args.Transfer.BytesTransferred}/{args.Transfer.Size} = {args.Transfer.PercentComplete}%) @ {args.Transfer.AverageSpeed.SizeSuffix()}/s" : string.Empty)}"); }; Client.TransferProgressUpdated += (e, args) => { // this is really verbose. // Console.WriteLine($"[{args.Transfer.Direction.ToString().ToUpper()}] [{args.Transfer.Username}/{Path.GetFileName(args.Transfer.Filename)}] {args.Transfer.BytesTransferred}/{args.Transfer.Size} {args.Transfer.PercentComplete}% {args.Transfer.AverageSpeed}kb/s"); }; // bind BrowseProgressUpdated to track progress of browse response payload transfers. // these can take a while depending on number of files shared. Client.BrowseProgressUpdated += (e, args) => { browseTracker.AddOrUpdate(args.Username, args); }; // bind UserStatusChanged to monitor the status of users added via AddUserAsync(). Client.UserStatusChanged += (e, args) => { // Console.WriteLine($"[USER] {args.Username}: {args.Status}"); }; Client.PrivateMessageReceived += (e, args) => { conversationTracker.AddOrUpdate(args.Username, PrivateMessage.FromEventArgs(args)); }; Client.RoomMessageReceived += (e, args) => { var message = RoomMessage.FromEventArgs(args, DateTime.UtcNow); roomTracker.AddOrUpdateMessage(args.RoomName, message); }; Client.RoomJoined += (e, args) => { if (args.Username != Username) // this will fire when we join a room; track that through the join operation. { roomTracker.TryAddUser(args.RoomName, args.UserData); } }; Client.RoomLeft += (e, args) => { roomTracker.TryRemoveUser(args.RoomName, args.Username); }; Client.Disconnected += async(e, args) => { Console.WriteLine($"Disconnected from Soulseek server: {args.Message}"); // don't reconnect if the disconnecting Exception is either of these types. // if KickedFromServerException, another client was most likely signed in, and retrying will cause a connect loop. // if ObjectDisposedException, the client is shutting down. if (!(args.Exception is KickedFromServerException || args.Exception is ObjectDisposedException)) { Console.WriteLine($"Attepting to reconnect..."); await Client.ConnectAsync(Username, Password); } }; Task.Run(async() => { await Client.ConnectAsync(Username, Password); }).GetAwaiter().GetResult(); Console.WriteLine($"Connected and logged in."); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider, ITransferTracker tracker) { if (!env.IsDevelopment()) { app.UseHsts(); } app.UseCors("AllowAll"); WebRoot = WebRoot ?? Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase).AbsolutePath), "wwwroot"); Console.WriteLine($"Serving static content from {WebRoot}"); app.UseFileServer(new FileServerOptions { FileProvider = new PhysicalFileProvider(WebRoot), RequestPath = "", EnableDirectoryBrowsing = false, EnableDefaultFiles = true }); app.UseMvc(); app.UseSwagger(options => { // use camelCasing for routes and properties options.PreSerializeFilters.Add((document, request) => { string camelCase(string key) => string.Join('/', key.Split('/').Select(x => x.Contains("{") || x.Length < 2 ? x : char.ToLowerInvariant(x[0]) + x.Substring(1))); document.Paths = document.Paths.ToDictionary(p => camelCase(p.Key), p => p.Value); document.Paths.ToList() .ForEach(path => typeof(PathItem).GetProperties().Where(p => p.PropertyType == typeof(Operation)).ToList() .ForEach(operation => ((Operation)operation.GetValue(path.Value, null))?.Parameters.ToList() .ForEach(prop => prop.Name = camelCase(prop.Name)))); }); }); app.UseSwaggerUI(options => provider.ApiVersionDescriptions.ToList() .ForEach(description => options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName))); // --------------------------------------------------------------------------------------------------------------------------------------------- // begin SoulseekClient implementation // --------------------------------------------------------------------------------------------------------------------------------------------- // create options for the client. // see the implementation of Func<> and Action<> options for detailed info. var clientOptions = new SoulseekClientOptions( listenPort: ListenPort, distributedChildLimit: 10, enableDistributedNetwork: false, minimumDiagnosticLevel: DiagnosticLevel.Debug, serverConnectionOptions: new ConnectionOptions(connectTimeout: 5000, inactivityTimeout: 15000), peerConnectionOptions: new ConnectionOptions(connectTimeout: 5000, inactivityTimeout: 15000), transferConnectionOptions: new ConnectionOptions(connectTimeout: 5000, inactivityTimeout: 15000), userInfoResponseResolver: UserInfoResponseResolver, browseResponseResolver: BrowseResponseResolver, enqueueDownloadAction: (username, endpoint, filename) => EnqueueDownloadAction(username, endpoint, filename, tracker), searchResponseResolver: SearchResponseResolver); Client = new SoulseekClient(options: clientOptions); // bind the DiagnosticGenerated event so we can trap and display diagnostic messages. this is optional, and if the event // isn't bound the minimumDiagnosticLevel should be set to None. Client.DiagnosticGenerated += (e, args) => { lock (ConsoleSyncRoot) { if (args.Level == DiagnosticLevel.Debug) { Console.ForegroundColor = ConsoleColor.DarkGray; } if (args.Level == DiagnosticLevel.Warning) { Console.ForegroundColor = ConsoleColor.Yellow; } Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss.fff")}] [DIAGNOSTIC:{e.GetType().Name}] [{args.Level}] {args.Message}"); Console.ResetColor(); } }; // bind transfer events. see TransferStateChangedEventArgs and TransferProgressEventArgs. Client.TransferStateChanged += (e, args) => Console.WriteLine($"[{args.Transfer.Direction.ToString().ToUpper()}] [{args.Transfer.Username}/{Path.GetFileName(args.Transfer.Filename)}] {args.PreviousState} => {args.Transfer.State}"); Client.TransferProgressUpdated += (e, args) => { // this is really verbose. // Console.WriteLine($"[{args.Transfer.Direction.ToString().ToUpper()}] [{args.Transfer.Username}/{Path.GetFileName(args.Transfer.Filename)}] {args.Transfer.BytesTransferred}/{args.Transfer.Size} {args.Transfer.PercentComplete}% {args.Transfer.AverageSpeed}kb/s"); }; // bind BrowseProgressUpdated to track progress of browse response payload transfers. // these can take a while depending on number of files shared. Client.BrowseProgressUpdated += (e, args) => Console.WriteLine($"[BROWSE] {args.Username}: {args.BytesTransferred} of {args.Size} ({args.PercentComplete}%)"); // bind UserStatusChanged to monitor the status of users added via AddUserAsync(). Client.UserStatusChanged += (e, args) => Console.WriteLine($"[USER] {args.Username}: {args.Status}"); Client.PrivateMessageReceived += (e, args) => Console.WriteLine($"[{args.Timestamp}] [PM]{(args.IsAdmin ? " [ADMIN]" : "")} {args.Username}: {args.Message}"); Client.Disconnected += async(e, args) => { Console.WriteLine($"Disconnected from Soulseek server: {args.Message}"); // don't reconnect if the disconnecting Exception is either of these types. // if KickedFromServerException, another client was most likely signed in, and retrying will cause a connect loop. // if ObjectDisposedException, the client is shutting down. if (!(args.Exception is KickedFromServerException || args.Exception is ObjectDisposedException)) { Console.WriteLine($"Attepting to reconnect..."); await Client.ConnectAsync(Username, Password); } }; Task.Run(async() => { await Client.ConnectAsync(Username, Password); }).GetAwaiter().GetResult(); Console.WriteLine($"Connected and logged in."); }
static async Task Main(string[] args) { using (var client = new SoulseekClient(new SoulseekClientOptions(minimumDiagnosticLevel: DiagnosticLevel.Debug))) { client.StateChanged += Client_ServerStateChanged; client.SearchResponseReceived += Client_SearchResponseReceived; client.SearchStateChanged += Client_SearchStateChanged; client.DownloadProgressUpdated += Client_DownloadProgress; client.DownloadStateChanged += Client_DownloadStateChanged; client.DiagnosticGenerated += Client_DiagnosticMessageGenerated; client.PrivateMessageReceived += Client_PrivateMessageReceived; await client.ConnectAsync(); Console.WriteLine("Enter username and password:"******"disconnect") { client.Disconnect(); return; } else if (cmd.StartsWith("msg")) { var arr = cmd.Split(' '); var peer = arr.Skip(1).Take(1).FirstOrDefault(); var message = arr.Skip(2).Take(999); await client.SendPrivateMessageAsync(peer, string.Join(' ', message)); } else if (cmd.StartsWith("browse")) { var peer = cmd.Split(' ').Skip(1).FirstOrDefault(); var result = await client.BrowseAsync(peer); Console.WriteLine(JsonConvert.SerializeObject(result)); continue; } else if (cmd.StartsWith("search")) { using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300))) { var search = string.Join(' ', cmd.Split(' ').Skip(1)); var token = new Random().Next(); var result = await client.SearchAsync(search, token, new SearchOptions( filterFiles : false, filterResponses : false, fileLimit : 10000), cts.Token); Console.WriteLine(JsonConvert.SerializeObject(result)); continue; } } else if (cmd.StartsWith("download-folder")) { var peer = cmd.Split(' ').Skip(1).FirstOrDefault(); var files = new[] { @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\01 - Bulls On Parade.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\02 - Down Rodeo.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\03 - People Of The Sun.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\04 - Revolver.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\05 - Roll Right.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\06 - Snakecharmer.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\07 - Tire Me.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\08 - Vietnow.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\09 - Wind Below.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\10 - Without A Face.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\11 - Year Of The Boomerang.mp3", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\Thumbs.db", @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\album.nfo", }; var task = Task.Run(() => { var random = new Random(); Parallel.ForEach(files, async(file) => { Console.WriteLine($"Attempting to download {file}"); var bytes = await client.DownloadAsync(peer, file, random.Next()); var filename = $@"C:\tmp\{Path.GetFileName(file)}"; Console.WriteLine($"Bytes received: {bytes.Length}; writing to file {filename}..."); System.IO.File.WriteAllBytes(filename, bytes); Console.WriteLine("Download complete!"); }); }); await task; Console.WriteLine($"All files complete."); } else if (cmd.StartsWith("download")) { var peer = cmd.Split(' ').Skip(1).FirstOrDefault(); var file = string.Join(' ', cmd.Split(' ').Skip(2)); var bytes = await client.DownloadAsync(peer, file, new Random().Next()); var filename = $@"C:\tmp\{Path.GetFileName(file)}"; Console.WriteLine($"Bytes received: {bytes.Length}; writing to file {filename}..."); System.IO.File.WriteAllBytes(filename, bytes); Console.WriteLine("Download complete!"); } else { try { await client.LoginAsync(cmd.Split(' ')[0], cmd.Split(' ')[1]); Console.WriteLine($"Logged in."); } catch (Exception ex) { Console.WriteLine($"Login failed: {ex.Message}"); } } } } }