diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj b/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj index 7c4d6a76..234e898b 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj +++ b/src/Plugins/BotSharp.Plugin.WebDriver/BotSharp.Plugin.WebDriver.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumInstance.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumInstance.cs new file mode 100644 index 00000000..43586be4 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumInstance.cs @@ -0,0 +1,88 @@ +using OpenQA.Selenium.Chrome; +using System.IO; + +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public class SeleniumInstance : IDisposable +{ + Dictionary _contexts = new Dictionary(); + + public Dictionary Contexts => _contexts; + + public INavigation GetPage(string id) + { + InitInstance(id).Wait(); + return _contexts[id].Navigate(); + } + + public string GetPageContent(string id) + { + InitInstance(id).Wait(); + return _contexts[id].PageSource; + } + + public async Task InitInstance(string id) + { + return await InitContext(id); + } + + public async Task InitContext(string id) + { + if (_contexts.ContainsKey(id)) + return _contexts[id]; + + string tempFolderPath = $"{Path.GetTempPath()}\\_selenium\\{id}"; + + var options = new ChromeOptions(); + options.AddArgument("disable-infobars"); + options.AddArgument($"--user-data-dir={tempFolderPath}"); + var selenium = new ChromeDriver(options); + selenium.Manage().Window.Maximize(); + selenium.Navigate().GoToUrl("about:blank"); + _contexts[id] = selenium; + + return _contexts[id]; + } + + public async Task NewPage(string id) + { + await InitContext(id); + var selenium = _contexts[id]; + selenium.Navigate().GoToUrl("about:blank"); + return _contexts[id].Navigate(); + } + + public async Task Wait(string id) + { + if (_contexts.ContainsKey(id)) + { + _contexts[id].Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10); + } + await Task.Delay(100); + } + + public async Task Close(string id) + { + if (_contexts.ContainsKey(id)) + { + _contexts[id].Quit(); + _contexts.Remove(id); + } + } + + public async Task CloseCurrentPage(string id) + { + if (_contexts.ContainsKey(id)) + { + } + } + + public void Dispose() + { + foreach(var context in _contexts) + { + context.Value.Quit(); + } + _contexts.Clear(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.DoAction.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.DoAction.cs new file mode 100644 index 00000000..bb9afd87 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.DoAction.cs @@ -0,0 +1,43 @@ +using OpenQA.Selenium; +using OpenQA.Selenium.Interactions; + +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task DoAction(MessageInfo message, ElementActionArgs action, BrowserActionResult result) + { + var driver = await _instance.InitInstance(message.ContextId); + IWebElement element = default; + if (result.Selector.StartsWith("//")) + { + element = driver.FindElement(By.XPath(result.Selector)); + } + else + { + element = driver.FindElement(By.CssSelector(result.Selector)); + } + + + if (action.Action == BroswerActionEnum.Click) + { + if (action.Position == null) + { + element.Click(); + } + else + { + var size = element.Size; + var actions = new Actions(driver); + actions.MoveToElement(element) + .MoveByOffset((int)action.Position.X - size.Width / 2, (int)action.Position.Y - size.Height / 2) + .Click() + .Perform(); + } + } + else if (action.Action == BroswerActionEnum.InputText) + { + element.SendKeys(action.Content); + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GetAttributeValue.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GetAttributeValue.cs new file mode 100644 index 00000000..ee5d47e7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GetAttributeValue.cs @@ -0,0 +1,18 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task GetAttributeValue(MessageInfo message, ElementLocatingArgs location, BrowserActionResult result) + { + var driver = await _instance.InitInstance(message.ContextId); + var locator = driver.FindElement(By.CssSelector(result.Selector)); + var value = string.Empty; + + if (!string.IsNullOrEmpty(location?.AttributeName)) + { + value = locator.GetAttribute(location.AttributeName); + } + + return value ?? string.Empty; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GoToPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GoToPage.cs new file mode 100644 index 00000000..abe51210 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.GoToPage.cs @@ -0,0 +1,27 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task GoToPage(string contextId, string url, bool openNewTab = false) + { + var result = new BrowserActionResult(); + try + { + var page = openNewTab ? await _instance.NewPage(contextId) : + _instance.GetPage(contextId); + page.GoToUrl(url); + await _instance.Wait(contextId); + + result.Body = _instance.GetPageContent(contextId); + result.IsSuccess = true; + } + catch (Exception ex) + { + result.Message = ex.Message; + result.StackTrace = ex.StackTrace; + _logger.LogError(ex.Message); + } + + return result; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LaunchBrowser.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LaunchBrowser.cs new file mode 100644 index 00000000..3203b617 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LaunchBrowser.cs @@ -0,0 +1,33 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + public async Task LaunchBrowser(string contextId, string? url, bool openIfNotExist = true) + { + var result = new BrowserActionResult() + { + IsSuccess = true + }; + var context = await _instance.InitInstance(contextId); + + if (!string.IsNullOrEmpty(url)) + { + // Check if the page is already open + var page = await _instance.NewPage(contextId); + + try + { + page.GoToUrl(url); + result.IsSuccess = true; + } + catch (Exception ex) + { + result.Message = ex.Message; + result.StackTrace = ex.StackTrace; + _logger.LogError(ex.Message); + } + } + + return result; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LocateElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LocateElement.cs new file mode 100644 index 00000000..4d96b9a6 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.LocateElement.cs @@ -0,0 +1,119 @@ +using OpenQA.Selenium; +using System.Collections.ObjectModel; + +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver +{ + /// + /// Using attributes or text to locate element and return the selector + /// + /// + /// + /// + public async Task LocateElement(MessageInfo message, ElementLocatingArgs location) + { + var result = new BrowserActionResult(); + var driver = await _instance.InitInstance(message.ContextId); + + IWebElement locator = driver.FindElement(By.TagName("body")); + ReadOnlyCollection elements = default; + string selector = string.Empty; + int count = 0; + + // check if selector is specified + if (location.Selector != null) + { + selector = location.Selector; + elements = driver.FindElements(By.CssSelector(location.Selector)); + count = elements.Count; + } + + // try attribute + if (count == 0 && !string.IsNullOrEmpty(location.AttributeName)) + { + selector = $"[{location.AttributeName}='{location.AttributeValue}']"; + elements = driver.FindElements(By.CssSelector(selector)); + count = elements.Count; + } + + // Retrieve the page raw html and infer the element path + if (!string.IsNullOrEmpty(location.Text)) + { + var regexExpression = location.MatchRule.ToLower() switch + { + "startwith" => $"^{location.Text}", + "endwith" => $"{location.Text}$", + "contains" => $"{location.Text}", + _ => $"^{location.Text}$" + }; + var regex = new Regex(regexExpression, RegexOptions.IgnoreCase); + + selector = $"//*[text() = '{location.Text}']"; + elements = driver.FindElements(By.XPath(selector)); + count = elements.Count; + + // try placeholder + if (count == 0) + { + selector = $"[placeholder='{location.Text}']"; + elements = driver.FindElements(By.CssSelector(selector)); + count = elements.Count; + } + } + + if (location.Index >= 0) + { + locator = elements[location.Index]; + count = 1; + } + + if (count == 0) + { + result.Message = $"Can't locate element by keyword {location.Text}"; + _logger.LogError(result.Message); + } + else if (count == 1) + { + locator = elements[0]; + result.Selector = selector; + var text = locator.Text; + result.Body = text; + result.IsSuccess = true; + } + else if (count > 1) + { + if (location.FailIfMultiple) + { + result.Message = $"Multiple elements are found by {locator}"; + _logger.LogError(result.Message); + + /*foreach (var element in await locator.AllAsync()) + { + var content = await element.InnerHTMLAsync(); + _logger.LogError(content); + }*/ + } + else + { + result.Selector = locator.ToString(); + result.IsSuccess = true; + } + } + + // Hightlight the element + if (result.IsSuccess && location.Highlight) + { + /*var handle = await page.QuerySelectorAsync(result.Selector); + + await page.EvaluateAsync($@" + (element) => {{ + element.style.outline = '2px solid red'; + }}", handle); + + result.IsHighlighted = true;*/ + } + + return result; + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.cs new file mode 100644 index 00000000..26ce8ea7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/SeleniumDriver/SeleniumWebDriver.cs @@ -0,0 +1,94 @@ +namespace BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; + +public partial class SeleniumWebDriver : IWebBrowser +{ + private readonly IServiceProvider _services; + private readonly SeleniumInstance _instance; + private readonly ILogger _logger; + public SeleniumInstance Instance => _instance; + + public Agent Agent => _agent; + private Agent _agent; + + public SeleniumWebDriver(IServiceProvider services, SeleniumInstance instance, ILogger logger) + { + _services = services; + _instance = instance; + _logger = logger; + } + + public Task ActionOnElement(MessageInfo message, ElementLocatingArgs location, ElementActionArgs action) + { + throw new NotImplementedException(); + } + + public Task ChangeCheckbox(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task ChangeListValue(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task CheckRadioButton(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task ClickButton(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task ClickElement(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task CloseBrowser(string contextId) + { + throw new NotImplementedException(); + } + + public Task CloseCurrentPage(string contextId) + { + throw new NotImplementedException(); + } + + public Task EvaluateScript(string contextId, string script) + { + throw new NotImplementedException(); + } + + public Task ExtractData(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task InputUserPassword(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task InputUserText(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task ScreenshotAsync(string contextId, string path) + { + throw new NotImplementedException(); + } + + public Task ScrollPageAsync(BrowserActionParams actionParams) + { + throw new NotImplementedException(); + } + + public Task SendHttpRequest(string contextId, HttpRequestParams actionParams) + { + throw new NotImplementedException(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Using.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Using.cs index 161e150a..619f0e23 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Using.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Using.cs @@ -9,6 +9,7 @@ global using Microsoft.Playwright; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; +global using OpenQA.Selenium; global using BotSharp.Abstraction.Browsing.Enums; global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Plugins; diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/WebDriverPlugin.cs b/src/Plugins/BotSharp.Plugin.WebDriver/WebDriverPlugin.cs index 2c7a4133..e767104b 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/WebDriverPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/WebDriverPlugin.cs @@ -1,4 +1,5 @@ using BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; +using BotSharp.Plugin.WebDriver.Drivers.SeleniumDriver; using BotSharp.Plugin.WebDriver.Hooks; namespace BotSharp.Plugin.Playwrights; @@ -13,8 +14,12 @@ public class WebDriverPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { - services.AddScoped(); - services.AddSingleton(); + // services.AddScoped(); + // services.AddSingleton(); + + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); services.AddScoped(); }