diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs index ff2c4a7d..9386dd8a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebBrowser.cs @@ -28,4 +28,5 @@ public interface IWebBrowser Task CloseCurrentPage(MessageInfo message); Task SendHttpRequest(MessageInfo message, HttpRequestParams actionParams); Task GetAttributeValue(MessageInfo message, ElementLocatingArgs location); + Task SetAttributeValue(MessageInfo message, ElementLocatingArgs location); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionArgs.cs index 21bdf6ab..7987b3a5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionArgs.cs @@ -3,4 +3,6 @@ namespace BotSharp.Abstraction.Browsing.Models; public class BrowserActionArgs { public bool Headless { get; set; } + public string? UserDataDir { get; set; } + public string? RemoteHostUrl { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs index 1867a687..e2f11972 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs @@ -8,6 +8,10 @@ public class BrowserActionResult public string? StackTrace { get; set; } public string? Selector { get; set; } public string? Body { get; set; } + /// + /// Page open in new tab after button click + /// + public string? UrlAfterAction { get; set; } public bool IsHighlighted { get; set; } public override string ToString() diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 4391f970..70fecc8a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -33,6 +33,7 @@ public interface IBotSharpRepository : IHaveServiceProvider List GetUserByIds(List ids) => throw new NotImplementedException(); List GetUsersByAffiliateId(string affiliateId) => throw new NotImplementedException(); User? GetUserByUserName(string userName) => throw new NotImplementedException(); + void UpdateUserName(string userId, string userName) => throw new NotImplementedException(); Dashboard? GetDashboard(string id = null) => throw new NotImplementedException(); void CreateUser(User user) => throw new NotImplementedException(); void UpdateExistUser(string userId, User user) => throw new NotImplementedException(); @@ -49,6 +50,7 @@ public interface IBotSharpRepository : IHaveServiceProvider PagedItems GetUsers(UserFilter filter) => throw new NotImplementedException(); User? GetUserDetails(string userId, bool includeAgent = false) => throw new NotImplementedException(); bool UpdateUser(User user, bool updateUserAgents = false) => throw new NotImplementedException(); + #endregion #region Agent diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs index 134ca25b..5e56b431 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs @@ -17,6 +17,7 @@ public interface IUserService Task GetAffiliateToken(string authorization); Task GetAdminToken(string authorization); Task GetToken(string authorization); + Task CreateTokenByUser(User user); Task GetMyProfile(); Task VerifyUserNameExisting(string userName); Task VerifyEmailExisting(string email); diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs index 7699a58b..8d136dc5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs @@ -23,7 +23,6 @@ public class User public bool Verified { get; set; } public string RegionCode { get; set; } = "CN"; public string? AffiliateId { get; set; } - public string? ReferralCode { get; set; } public string? EmployeeId { get; set; } public bool IsDisabled { get; set; } public IEnumerable Permissions { get; set; } = []; diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserActivationModel.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserActivationModel.cs index 904bd936..3ea4a915 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserActivationModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserActivationModel.cs @@ -4,4 +4,5 @@ public class UserActivationModel { public string UserName { get; set; } public string VerificationCode { get; set; } + public string RegionCode { get; set; } = "CN"; } diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 1c9955aa..d1232c5e 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -190,10 +190,10 @@ - + - + diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisPublisher.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisPublisher.cs index 89de2453..cee5cc6f 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisPublisher.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisPublisher.cs @@ -39,10 +39,7 @@ public class RedisPublisher : IEventPublisher // Add a message to the stream, keeping only the latest 1 million messages var messageId = await db.StreamAddAsync(channel, - [ - new NameValueEntry("message", message), - new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o")) - ], + AssembleMessage(message), maxLength: 1000 * 10000); _logger.LogInformation($"Published message {channel} {message} ({messageId})"); @@ -82,6 +79,25 @@ public class RedisPublisher : IEventPublisher return exists; } + public static NameValueEntry[] AssembleMessage(RedisValue message) + { + return + [ + new NameValueEntry("message", message), + new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o")) + ]; + } + + public static NameValueEntry[] AssembleErrorMessage(RedisValue message, string error) + { + return + [ + new NameValueEntry("message", message), + new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o")), + new NameValueEntry("error", error) + ]; + } + public async Task ReDispatchAsync(string channel, int count = 10, string order = "asc") { var db = _redis.GetDatabase(); @@ -93,10 +109,10 @@ public class RedisPublisher : IEventPublisher try { - var messageId = await db.StreamAddAsync(channel, [ - new NameValueEntry("message", entry.Values[0].Value), - new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o")) - ]); + var message = entry.Values.First(x => x.Name == "message").Value; + var messageId = await db.StreamAddAsync(channel, + AssembleMessage(message), + maxLength: 1000 * 10000); _logger.LogWarning($"ReDispatched message: {channel} {entry.Values[0].Value} ({messageId})"); @@ -164,20 +180,7 @@ public class RedisPublisher : IEventPublisher var db = _redis.GetDatabase(); var entries = await db.StreamRangeAsync(channel, "-", "+", count: count, messageOrder: Order.Ascending); - foreach (var entry in entries) - { - _logger.LogInformation($"Fetched message: {channel} {entry.Values[0].Value} ({entry.Id})"); - - try - { - await db.StreamDeleteAsync(channel, [entry.Id]); - - _logger.LogWarning($"Deleted message: {channel} {entry.Values[0].Value} ({entry.Id})"); - } - catch (Exception ex) - { - _logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}\r\n{ex}"); - } - } + var deletedCount = await db.StreamDeleteAsync(channel, entries.Select(x => x.Id).ToArray()); + _logger.LogWarning($"Deleted {deletedCount} messages from Redis stream {channel}"); } } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisSubscriber.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisSubscriber.cs index 9a3c9982..61db1d33 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisSubscriber.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisSubscriber.cs @@ -42,6 +42,8 @@ public class RedisSubscriber : IEventSubscriber await CreateConsumerGroup(db, channel, group); } + await CreateConsumerGroup(db, $"{channel}-Error", group); + var consumer = Environment.MachineName; if (port.HasValue) { @@ -58,28 +60,36 @@ public class RedisSubscriber : IEventSubscriber break; } - if (priorityEnabled) + try { - if (await HandleGroupMessage(db, $"{channel}-{EventPriority.High}", group, consumer, received) > 0) + if (priorityEnabled) { - continue; - } + if (await HandleGroupMessage(db, $"{channel}-{EventPriority.High}", group, consumer, received, $"{channel}-Error") > 0) + { + continue; + } - if (await HandleGroupMessage(db, $"{channel}-{EventPriority.Medium}", group, consumer, received) > 0) + if (await HandleGroupMessage(db, $"{channel}-{EventPriority.Medium}", group, consumer, received, $"{channel}-Error") > 0) + { + continue; + } + + await HandleGroupMessage(db, $"{channel}-{EventPriority.Low}", group, consumer, received, $"{channel}-Error"); + } + else { - continue; + await HandleGroupMessage(db, channel, group, consumer, received, $"{channel}-Error"); } - - await HandleGroupMessage(db, $"{channel}-{EventPriority.Low}", group, consumer, received); } - else + catch (Exception ex) { - await HandleGroupMessage(db, channel, group, consumer, received); + _logger.LogError($"Error processing message: {ex.Message}\r\n{ex}"); + await Task.Delay(1000 * 60); } } } - private async Task HandleGroupMessage(IDatabase db, string channel, string group, string consumer, Func received) + private async Task HandleGroupMessage(IDatabase db, string channel, string group, string consumer, Func received, string errorChannel) { var entries = await db.StreamReadGroupAsync(channel, group, consumer, count: 1); foreach (var entry in entries) @@ -90,13 +100,24 @@ public class RedisSubscriber : IEventSubscriber try { await received(channel, entry.Values[0].Value); - - // Optionally delete the message to save space - await db.StreamDeleteAsync(channel, [entry.Id]); } catch (Exception ex) { - _logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}\r\n{ex}"); + _logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id} {entry.Values[0].Value}"); + + // Add a message to the Error stream, keeping only the latest 1 million messages + await db.StreamAddAsync(errorChannel, + RedisPublisher.AssembleErrorMessage(entry.Values[0].Value, ex.Message), + messageId: entry.Id, + maxLength: 1000 * 10000); + + // Slow down the consumer if there are errors + await Task.Delay(1000 * 10); + } + finally + { + var deletedCount = await db.StreamDeleteAsync(channel, [entry.Id]); + _logger.LogInformation($"Handled message {entry.Id}: {deletedCount == 1}"); } } diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index 9bc436a7..18deb7d8 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -62,7 +62,7 @@ public class UserService : IUserService if (!string.IsNullOrWhiteSpace(user.Phone)) { - record = db.GetUserByPhone(user.Phone); + record = db.GetUserByPhone(user.Phone, regionCode: (string.IsNullOrWhiteSpace(user.RegionCode) ? "CN" : user.RegionCode)); } if (record == null && !string.IsNullOrWhiteSpace(user.Email)) @@ -112,7 +112,7 @@ public class UserService : IUserService db.UpdateExistUser(hasRegisterId, record); } - _logger.LogWarning($"Created new user account: {record.Id} {record.UserName}"); + _logger.LogWarning($"Created new user account: {record.Id} {record.UserName}, RegionCode: {record.RegionCode}"); Utilities.ClearCache(); var hooks = _services.GetServices(); @@ -127,7 +127,8 @@ public class UserService : IUserService public async Task UpdatePassword(string password, string verificationCode) { var db = _services.GetRequiredService(); - var record = db.GetUserByUserName(_user.UserName); + + var record = db.GetUserById(_user.Id); if (record == null) { @@ -473,7 +474,7 @@ public class UserService : IUserService var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id); if (record == null) { - record = db.GetUserByPhone(id); + record = db.GetUserByPhone(id, regionCode: (string.IsNullOrWhiteSpace(model.RegionCode) ? "CN" : model.RegionCode)); } if (record == null) @@ -505,6 +506,20 @@ public class UserService : IUserService return token; } + public async Task CreateTokenByUser(User user) + { + var accessToken = GenerateJwtToken(user); + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); + var token = new Token + { + AccessToken = accessToken, + ExpireTime = jwt.Payload.Exp.Value, + TokenType = "Bearer", + Scope = "api" + }; + return token; + } + public async Task VerifyUserNameExisting(string userName) { if (string.IsNullOrEmpty(userName)) @@ -646,7 +661,7 @@ public class UserService : IUserService if (!string.IsNullOrEmpty(user.Phone)) { - record = db.GetUserByPhone(user.Phone); + record = db.GetUserByPhone(user.Phone, regionCode: (string.IsNullOrWhiteSpace(user.RegionCode) ? "CN" : user.RegionCode)); } if (record == null) @@ -695,7 +710,7 @@ public class UserService : IUserService var curUser = await GetMyProfile(); var db = _services.GetRequiredService(); var record = db.GetUserById(curUser.Id); - var existPhone = db.GetUserByPhone(phone); + var existPhone = db.GetUserByPhone(phone, regionCode: regionCode); if (record == null || (existPhone != null && existPhone.RegionCode == regionCode)) { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs index 9d652a3e..75b33c73 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Settings; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; @@ -244,7 +243,7 @@ public class UserController : ControllerBase var file = fileStorage.GetUserAvatar(); if (string.IsNullOrEmpty(file)) { - return NotFound(); + return NoContent(); } return BuildFileResult(file); } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs index b529a832..4d2594b8 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs @@ -13,7 +13,6 @@ public class UserCreationModel public string Type { get; set; } = UserType.Client; public string Role { get; set; } = UserRole.User; public string RegionCode { get; set; } = "CN"; - public string? ReferralCode { get; set; } public User ToUser() { return new User @@ -27,7 +26,6 @@ public class UserCreationModel Role = Role, Type = Type, RegionCode = RegionCode, - ReferralCode = ReferralCode }; } } diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj b/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj index 7dc0ac87..61ab8648 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj @@ -33,10 +33,6 @@ - - - - diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj b/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj index 88709238..5ac884e0 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index 89eb18ad..82e9f0d4 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -36,7 +36,7 @@ public class MongoDbContext Key = x.Split("=")[0], Value = x.Split("=")[1] }).ToList(); - + var source = queries.FirstOrDefault(x => x.Key.IsEqualTo(DB_NAME_INDEX)); if (source != null) { @@ -168,4 +168,5 @@ public class MongoDbContext public IMongoCollection CrontabItems => Database.GetCollection($"{_collectionPrefix}_CronTabItems"); + } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs index 3c76e21d..29bd36be 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs @@ -112,6 +112,14 @@ public partial class MongoRepository _dc.Users.UpdateOne(filter, update); } + public void UpdateUserName(string userId, string userName) + { + var filter = Builders.Filter.Eq(x => x.Id, userId); + var update = Builders.Update + .Set(x => x.UserName, userName); + _dc.Users.UpdateOne(filter, update); + } + public void UpdateUserVerified(string userId) { var filter = Builders.Filter.Eq(x => x.Id, userId); @@ -151,7 +159,7 @@ public partial class MongoRepository var update = Builders.Update.Set(x => x.Phone, phone) .Set(x => x.UpdatedTime, DateTime.UtcNow) .Set(x => x.RegionCode, regionCode) - .Set(x => x.UserName, phone) + //.Set(x => x.UserName, phone) .Set(x => x.FirstName, phone); _dc.Users.UpdateOne(filter, update); } @@ -331,10 +339,10 @@ public partial class MongoRepository .FirstOrDefault(x => x.Id == userId || (x.ExternalId != null && x.ExternalId == userId)); if (user == null) return; var curDash = user.Dashboard ?? new Dashboard(); - curDash.ConversationList.Add(new DashboardConversation - { + curDash.ConversationList.Add(new DashboardConversation + { Id = Guid.NewGuid().ToString(), - ConversationId = conversationId + ConversationId = conversationId }); var filter = Builders.Filter.Eq(x => x.Id, userId); diff --git a/src/Plugins/BotSharp.Plugin.Selenium/BotSharp.Plugin.Selenium.csproj b/src/Plugins/BotSharp.Plugin.Selenium/BotSharp.Plugin.Selenium.csproj new file mode 100644 index 00000000..2a4e3e08 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Selenium/BotSharp.Plugin.Selenium.csproj @@ -0,0 +1,22 @@ + + + + $(TargetFramework) + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + $(SolutionDir)packages + + + + + + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumInstance.cs b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumInstance.cs similarity index 97% rename from src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumInstance.cs rename to src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumInstance.cs index 5e1f5eaf..253ba487 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumInstance.cs +++ b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumInstance.cs @@ -1,7 +1,7 @@ using OpenQA.Selenium.Chrome; using System.IO; -namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; +namespace BotSharp.Plugin.Selenium.Drivers; public class SeleniumInstance : IDisposable { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.DoAction.cs b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.DoAction.cs similarity index 93% rename from src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.DoAction.cs rename to src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.DoAction.cs index bb9afd87..931e6736 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.DoAction.cs +++ b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.DoAction.cs @@ -1,7 +1,6 @@ -using OpenQA.Selenium; using OpenQA.Selenium.Interactions; -namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; +namespace BotSharp.Plugin.Selenium.Drivers; public partial class SeleniumWebDriver { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.EvaluateScript.cs b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.EvaluateScript.cs similarity index 86% rename from src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.EvaluateScript.cs rename to src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.EvaluateScript.cs index 9a59cb81..3fb6b39a 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.EvaluateScript.cs +++ b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.EvaluateScript.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; +namespace BotSharp.Plugin.Selenium.Drivers; public partial class SeleniumWebDriver { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GetAttributeValue.cs b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.GetAttributeValue.cs similarity index 91% rename from src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GetAttributeValue.cs rename to src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.GetAttributeValue.cs index 0402b2db..ff99d29d 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GetAttributeValue.cs +++ b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.GetAttributeValue.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; +namespace BotSharp.Plugin.Selenium.Drivers; public partial class SeleniumWebDriver { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GoToPage.cs b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.GoToPage.cs similarity index 92% rename from src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GoToPage.cs rename to src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.GoToPage.cs index db7d36a3..15a71480 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GoToPage.cs +++ b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.GoToPage.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; +namespace BotSharp.Plugin.Selenium.Drivers; public partial class SeleniumWebDriver { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.HttpRequest.cs b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.HttpRequest.cs similarity index 95% rename from src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.HttpRequest.cs rename to src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.HttpRequest.cs index c9940318..affeaf62 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.HttpRequest.cs +++ b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.HttpRequest.cs @@ -1,6 +1,6 @@ using System.Net.Http; -namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; +namespace BotSharp.Plugin.Selenium.Drivers; public partial class SeleniumWebDriver { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LaunchBrowser.cs b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.LaunchBrowser.cs similarity index 85% rename from src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LaunchBrowser.cs rename to src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.LaunchBrowser.cs index 4e8b6498..9c104a59 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LaunchBrowser.cs +++ b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.LaunchBrowser.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; +namespace BotSharp.Plugin.Selenium.Drivers; public partial class SeleniumWebDriver { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LocateElement.cs b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.LocateElement.cs similarity index 97% rename from src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LocateElement.cs rename to src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.LocateElement.cs index 4d96b9a6..184106a5 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LocateElement.cs +++ b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.LocateElement.cs @@ -1,7 +1,6 @@ -using OpenQA.Selenium; using System.Collections.ObjectModel; -namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; +namespace BotSharp.Plugin.Selenium.Drivers; public partial class SeleniumWebDriver { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.cs b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.cs similarity index 93% rename from src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.cs rename to src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.cs index 15dd0bc1..0e6dd0a3 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.cs +++ b/src/Plugins/BotSharp.Plugin.Selenium/Drivers/SeleniumWebDriver.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; +namespace BotSharp.Plugin.Selenium.Drivers; public partial class SeleniumWebDriver : IWebBrowser { @@ -86,4 +86,9 @@ public partial class SeleniumWebDriver : IWebBrowser { throw new NotImplementedException(); } + + public Task PressKey(MessageInfo message, string key) + { + throw new NotImplementedException(); + } } diff --git a/src/Plugins/BotSharp.Plugin.Selenium/Using.cs b/src/Plugins/BotSharp.Plugin.Selenium/Using.cs new file mode 100644 index 00000000..abadf8fa --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Selenium/Using.cs @@ -0,0 +1,23 @@ +global using System; +global using System.Collections.Generic; +global using System.Text; +global using System.Threading.Tasks; +global using System.Text.Json; +global using System.Linq; +global using System.Text.RegularExpressions; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using BotSharp.Abstraction.Browsing.Enums; +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Plugins; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Functions; +global using BotSharp.Abstraction.Agents.Models; +global using BotSharp.Abstraction.Templating; +global using BotSharp.Abstraction.Agents; +global using BotSharp.Abstraction.Utilities; +global using BotSharp.Abstraction.Browsing.Models; +global using BotSharp.Abstraction.Browsing; + +global using OpenQA.Selenium; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs index bb008319..231d793a 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs @@ -1,13 +1,9 @@ -using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Messaging.Enums; using BotSharp.Abstraction.Messaging.Models.RichContent.Template; using BotSharp.Abstraction.Messaging.Models.RichContent; using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Planning; -using BotSharp.Abstraction.Routing; using BotSharp.Core.Infrastructures; -using System.Text.RegularExpressions; -using BotSharp.Plugin.SqlDriver.Interfaces; namespace BotSharp.Plugin.SqlDriver.Hooks; diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj b/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj index 296e17c3..15c2868c 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj +++ b/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj @@ -3,7 +3,7 @@ $(TargetFramework) enable - 12.0 + $(LangVersion) $(BotSharpVersion) $(GeneratePackageOnBuild) $(GenerateDocumentationFile) @@ -11,16 +11,7 @@ - - - - - - - - - - + diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs index b5155cd5..95b88eee 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs @@ -45,28 +45,36 @@ public class PlaywrightInstance : IDisposable _playwright = await Playwright.CreateAsync(); } - string tempFolderPath = $"{Path.GetTempPath()}\\playwright\\{ctxId}"; - - _contexts[ctxId] = await _playwright.Chromium.LaunchPersistentContextAsync(tempFolderPath, new BrowserTypeLaunchPersistentContextOptions + if (!string.IsNullOrEmpty(args.RemoteHostUrl)) { - Headless = args.Headless, - Channel = "chrome", - ViewportSize = new ViewportSize + var browser = await _playwright.Chromium.ConnectOverCDPAsync(args.RemoteHostUrl); + _contexts[ctxId] = browser.Contexts[0]; + } + else + { + string userDataDir = args.UserDataDir ?? $"{Path.GetTempPath()}\\playwright\\{ctxId}"; + _contexts[ctxId] = await _playwright.Chromium.LaunchPersistentContextAsync(userDataDir, new BrowserTypeLaunchPersistentContextOptions { - Width = 1600, - Height = 900 - }, - IgnoreDefaultArgs = - [ - "--enable-automation", - ], - Args = - [ - "--disable-infobars", - "--test-type" - // "--start-maximized" - ] - }); + Headless = args.Headless, + Channel = "chrome", + ViewportSize = new ViewportSize + { + Width = 1600, + Height = 900 + }, + IgnoreDefaultArgs = + [ + "--enable-automation", + ], + Args = + [ + "--disable-infobars", + "--test-type" + // "--start-maximized" + ] + }); + } + _pages[ctxId] = new List(); _contexts[ctxId].Page += async (sender, page) => @@ -100,12 +108,7 @@ public class PlaywrightInstance : IDisposable return _contexts[ctxId]; } - public async Task NewPage(MessageInfo message, - bool enableResponseCallback = false, - bool responseInMemory = false, - List? responseContainer = null, - string[]? excludeResponseUrls = null, - string[]? includeResponseUrls = null) + public async Task NewPage(MessageInfo message, PageActionArgs args) { var context = await GetContext(message.ContextId); var page = await context.NewPageAsync(); @@ -115,70 +118,78 @@ public class PlaywrightInstance : IDisposable var js = @"Object.defineProperties(navigator, {webdriver:{get:()=>false}});"; await page.AddInitScriptAsync(js); - if (!enableResponseCallback) + if (!args.EnableResponseCallback) { return page; } page.Response += async (sender, e) => { - if (e.Status != 204 && - e.Headers.ContainsKey("content-type") && - (e.Request.ResourceType == "fetch" || e.Request.ResourceType == "xhr") && - (excludeResponseUrls == null || !excludeResponseUrls.Any(url => e.Url.ToLower().Contains(url))) && - (includeResponseUrls == null || includeResponseUrls.Any(url => e.Url.ToLower().Contains(url)))) - { - Serilog.Log.Information($"{e.Request.Method}: {e.Url}"); - - try - { - var result = new WebPageResponseData - { - Url = e.Url.ToLower(), - PostData = e.Request?.PostData ?? string.Empty, - ResponseInMemory = responseInMemory - }; - - if (e.Headers["content-type"].Contains("application/json")) - { - if (e.Status == 200 && e.Ok) - { - var json = await e.JsonAsync(); - result.ResponseData = JsonSerializer.Serialize(json); - } - } - else - { - var html = await e.TextAsync(); - result.ResponseData = html; - } - - if (responseContainer != null && responseInMemory) - { - responseContainer.Add(result); - } - - Serilog.Log.Warning($"Response status: {e.Status} {e.StatusText}, OK: {e.Ok}"); - var webPageResponseHooks = _services.GetServices(); - foreach (var hook in webPageResponseHooks) - { - hook.OnDataFetched(message, result); - } - } - catch (ObjectDisposedException ex) - { - Serilog.Log.Information(ex.Message); - } - catch (Exception ex) - { - Serilog.Log.Error($"{e.Url}\r\n" + ex.ToString()); - } - } + await HandleFetchResponse(e, message, args); }; return page; } + public async Task HandleFetchResponse(IResponse response, MessageInfo message, PageActionArgs args) + { + if (response.Status != 204 && + response.Headers.ContainsKey("content-type") && + (response.Request.ResourceType == "fetch" || response.Request.ResourceType == "xhr") && + (args.ExcludeResponseUrls == null || !args.ExcludeResponseUrls.Any(url => response.Url.ToLower().Contains(url))) && + (args.IncludeResponseUrls == null || args.IncludeResponseUrls.Any(url => response.Url.ToLower().Contains(url)))) + { + Serilog.Log.Information($"{response.Request.Method}: {response.Url}"); + + try + { + var result = new WebPageResponseData + { + Url = response.Url.ToLower(), + PostData = response.Request?.PostData ?? string.Empty, + ResponseInMemory = args.ResponseInMemory + }; + + var html = await response.TextAsync(); + if (response.Headers["content-type"].Contains("application/json")) + { + if (response.Status == 200 && response.Ok) + { + if (!string.IsNullOrWhiteSpace(html)) + { + var json = await response.JsonAsync(); + result.ResponseData = JsonSerializer.Serialize(json); + } + } + } + else + { + result.ResponseData = html; + } + + if (args.ResponseContainer != null && args.ResponseInMemory) + { + args.ResponseContainer.Add(result); + } + + Serilog.Log.Warning($"Response status: {response.Status} {response.StatusText}, OK: {response.Ok}"); + var webPageResponseHooks = _services.GetServices(); + foreach (var hook in webPageResponseHooks) + { + hook.OnDataFetched(message, result); + } + } + catch (ObjectDisposedException ex) + { + Serilog.Log.Information(ex.Message); + } + catch (Exception ex) + { + Serilog.Log.Error($"{response.Url}\r\n" + ex.ToString()); + } + } + } + /// /// Wait page and network until timeout in seconds /// diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs index ab1a0731..7a5f4820 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ActionOnElement.cs @@ -9,6 +9,7 @@ public partial class PlaywrightWebDriver if (result.IsSuccess) { await DoAction(message, action, result); + result.UrlAfterAction = _instance.GetPage(message.ContextId)?.Url; } return result; } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs index 40fec84e..b851cd1a 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs @@ -113,6 +113,7 @@ public partial class PlaywrightWebDriver } // Release mouse button + await Task.Delay(1000); await mouse.UpAsync(); } else diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GetAttributeValue.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GetAttributeValue.cs index 035bcf3b..ef266d08 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GetAttributeValue.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GetAttributeValue.cs @@ -19,4 +19,25 @@ public partial class PlaywrightWebDriver Body = value ?? string.Empty }; } + + public async Task SetAttributeValue(MessageInfo message, ElementLocatingArgs location) + { + var page = _instance.GetPage(message.ContextId); + ILocator locator = page.Locator(location.Selector); + var elementCount = await locator.CountAsync(); + + if (elementCount > 0) + { + foreach (var element in await locator.AllAsync()) + { + var script = $"element => element.{location.AttributeName} = '{location.AttributeValue}'"; + var result = await locator.EvaluateAsync(script); + } + } + + return new BrowserActionResult + { + IsSuccess = true + }; + } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs index 2f339ca6..7f0501cf 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs @@ -8,32 +8,41 @@ public partial class PlaywrightWebDriver var context = await _instance.GetContext(message.ContextId); try { - var page = await _instance.NewPage(message, enableResponseCallback: args.EnableResponseCallback, - responseInMemory: args.ResponseInMemory, - responseContainer: args.ResponseContainer, - excludeResponseUrls: args.ExcludeResponseUrls, - includeResponseUrls: args.IncludeResponseUrls); - - Serilog.Log.Information($"goto page: {args.Url}"); - - if (args.OpenNewTab && page != null && page.Url == "about:blank") + IPage? page = null; + if (!args.OpenNewTab) { - page = await _instance.NewPage(message, - enableResponseCallback: args.EnableResponseCallback, - responseInMemory: args.ResponseInMemory, - responseContainer: args.ResponseContainer, - excludeResponseUrls: args.ExcludeResponseUrls, - includeResponseUrls: args.IncludeResponseUrls); + page = _instance.Contexts[message.ContextId].Pages.LastOrDefault(); + + if (page != null) + { + await page.EvaluateAsync(@"() => { + window.open('', '_blank'); + }"); + + if (args.EnableResponseCallback) + { + page.Response += async (sender, e) => + { + await _instance.HandleFetchResponse(e, message, args); + }; + } + } + } + else + { + page = await _instance.NewPage(message, args); + + Serilog.Log.Information($"goto page: {args.Url}"); + + if (args.OpenNewTab && page != null && page.Url == "about:blank") + { + page = await _instance.NewPage(message, args); + } } if (page == null) { - page = await _instance.NewPage(message, - enableResponseCallback: args.EnableResponseCallback, - responseInMemory: args.ResponseInMemory, - responseContainer: args.ResponseContainer, - excludeResponseUrls: args.ExcludeResponseUrls, - includeResponseUrls: args.IncludeResponseUrls); + page = await _instance.NewPage(message, args); } // Active current tab diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs index 8695fc5c..613d2b22 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs @@ -14,6 +14,13 @@ public partial class PlaywrightWebDriver { var result = new BrowserActionResult(); var page = _instance.GetPage(message.ContextId); + if (page == null) + { + return new BrowserActionResult + { + IsSuccess = false + }; + } ILocator locator = page.Locator("body"); int count = 0; diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/OpenBrowserFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/OpenBrowserFn.cs index 6f4c3654..6a5a1ac1 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/OpenBrowserFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/OpenBrowserFn.cs @@ -1,5 +1,3 @@ -using SQLitePCL; - namespace BotSharp.Plugin.WebDriver.Functions; public class OpenBrowserFn : IFunctionCallback diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/WebPageHelper.cs b/src/Plugins/BotSharp.Plugin.WebDriver/WebPageHelper.cs index 113403eb..aaf61aa4 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/WebPageHelper.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/WebPageHelper.cs @@ -1,10 +1,25 @@ using HtmlAgilityPack; -using System.Net.Http; namespace BotSharp.Plugin.WebDriver; public static class WebPageHelper { + public static HtmlNode GetElement(string html, string selector) + { + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(html); + + return htmlDoc.DocumentNode.SelectSingleNode(selector); + } + + public static HtmlNodeCollection GetElements(string html, string selector) + { + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(html); + + return htmlDoc.DocumentNode.SelectNodes(selector); + } + public static string RemoveElements(string html, string selector) { var htmlDoc = new HtmlDocument();