diff --git a/Bot.sln b/Bot.sln
index 47ddcb13..d6f8137c 100644
--- a/Bot.sln
+++ b/Bot.sln
@@ -1,5 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
+#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platform.Bot", "csharp\Platform.Bot\Platform.Bot.csproj", "{81D55AD3-9698-4BEC-B7EC-26ED7B8E6E53}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileManager", "csharp\FileManager\FileManager.csproj", "{2F40CB1A-A1FD-4433-B998-BC3AEA2EFEB3}"
@@ -10,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage", "csharp\Storage\S
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraderBot", "csharp\TraderBot\TraderBot.csproj", "{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csharp", "csharp", "{5B274C13-EB2B-4612-9430-26107BEFA67F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platform.Bot.Tests", "csharp\Platform.Bot.Tests\Platform.Bot.Tests.csproj", "{F3D39934-E504-4B61-91DB-B37F654B7925}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -36,5 +41,12 @@ Global
{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F3D39934-E504-4B61-91DB-B37F654B7925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F3D39934-E504-4B61-91DB-B37F654B7925}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F3D39934-E504-4B61-91DB-B37F654B7925}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F3D39934-E504-4B61-91DB-B37F654B7925}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {F3D39934-E504-4B61-91DB-B37F654B7925} = {5B274C13-EB2B-4612-9430-26107BEFA67F}
EndGlobalSection
EndGlobal
diff --git a/csharp/Platform.Bot.Tests/GoogleSearchErrorTriggerTests.cs b/csharp/Platform.Bot.Tests/GoogleSearchErrorTriggerTests.cs
new file mode 100644
index 00000000..125e1a1f
--- /dev/null
+++ b/csharp/Platform.Bot.Tests/GoogleSearchErrorTriggerTests.cs
@@ -0,0 +1,55 @@
+using System.Threading.Tasks;
+using Moq;
+using Platform.Bot.Triggers;
+using Storage.Remote.GitHub;
+using Xunit;
+
+namespace Platform.Bot.Tests
+{
+ ///
+ ///
+ /// Tests for the GoogleSearchErrorTrigger class.
+ ///
+ ///
+ ///
+ public class GoogleSearchErrorTriggerTests
+ {
+ ///
+ ///
+ /// Tests that the trigger can be instantiated without errors.
+ ///
+ ///
+ ///
+ [Fact]
+ public void Constructor_WithValidStorage_CreatesInstance()
+ {
+ // Arrange
+ var mockStorage = new Mock("testuser", "testtoken", "testapp");
+
+ // Act
+ var trigger = new GoogleSearchErrorTrigger(mockStorage.Object);
+
+ // Assert
+ Assert.NotNull(trigger);
+ }
+
+ ///
+ ///
+ /// Tests that the class has the expected public methods.
+ ///
+ ///
+ ///
+ [Fact]
+ public void PublicMethods_AreAvailable()
+ {
+ // Arrange
+ var mockStorage = new Mock("testuser", "testtoken", "testapp");
+ var trigger = new GoogleSearchErrorTrigger(mockStorage.Object);
+
+ // Act & Assert - Just verify methods exist and can be called
+ Assert.NotNull(trigger.GetType().GetMethod("Condition"));
+ Assert.NotNull(trigger.GetType().GetMethod("Action"));
+ Assert.NotNull(trigger.GetType().GetMethod("Dispose"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/Platform.Bot.Tests/Platform.Bot.Tests.csproj b/csharp/Platform.Bot.Tests/Platform.Bot.Tests.csproj
new file mode 100644
index 00000000..68349681
--- /dev/null
+++ b/csharp/Platform.Bot.Tests/Platform.Bot.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8
+ enable
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/csharp/Platform.Bot/Platform.Bot.csproj b/csharp/Platform.Bot/Platform.Bot.csproj
index 2828772d..c331d31b 100644
--- a/csharp/Platform.Bot/Platform.Bot.csproj
+++ b/csharp/Platform.Bot/Platform.Bot.csproj
@@ -13,6 +13,7 @@
+
diff --git a/csharp/Platform.Bot/Program.cs b/csharp/Platform.Bot/Program.cs
index 521a6b95..7bb098cf 100644
--- a/csharp/Platform.Bot/Program.cs
+++ b/csharp/Platform.Bot/Program.cs
@@ -95,7 +95,7 @@ private static async Task Main(string[] args)
var dbContext = new FileStorage(databaseFilePath?.FullName ?? new TemporaryFile().Filename);
Console.WriteLine($"Bot has been started. {Environment.NewLine}Press CTRL+C to close");
var githubStorage = new GitHubStorage(githubUserName, githubApiToken, githubApplicationName);
- var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
+ var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new GoogleSearchErrorTrigger(githubStorage), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
var pullRequenstTracker = new PullRequestTracker(githubStorage, new MergeDependabotBumpsTrigger(githubStorage));
var timestampTracker = new DateTimeTracker(githubStorage, new CreateAndSaveOrganizationRepositoriesMigrationTrigger(githubStorage, dbContext, Path.Combine(Directory.GetCurrentDirectory(), "/github-migrations")));
var cancellation = new CancellationTokenSource();
diff --git a/csharp/Platform.Bot/Triggers/GoogleSearchErrorTrigger.cs b/csharp/Platform.Bot/Triggers/GoogleSearchErrorTrigger.cs
new file mode 100644
index 00000000..c2779b97
--- /dev/null
+++ b/csharp/Platform.Bot/Triggers/GoogleSearchErrorTrigger.cs
@@ -0,0 +1,428 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Net;
+using Interfaces;
+using Octokit;
+using Storage.Remote.GitHub;
+
+namespace Platform.Bot.Triggers
+{
+ using TContext = Issue;
+
+ ///
+ ///
+ /// Represents a trigger that detects errors in gist/pastebin URLs and searches Google for solutions.
+ ///
+ ///
+ ///
+ ///
+ public class GoogleSearchErrorTrigger : ITrigger
+ {
+ private readonly GitHubStorage _storage;
+ private readonly HttpClient _httpClient;
+ private readonly string[] _whitelistedDomains = new[]
+ {
+ "stackoverflow.com",
+ "github.com",
+ "docs.microsoft.com",
+ "developer.mozilla.org",
+ "msdn.microsoft.com",
+ "programmingpedia.com",
+ "geeksforgeeks.org"
+ };
+
+ ///
+ ///
+ /// Initializes a new instance.
+ ///
+ ///
+ ///
+ ///
+ /// A GitHub storage instance.
+ ///
+ ///
+ public GoogleSearchErrorTrigger(GitHubStorage storage)
+ {
+ _storage = storage;
+ _httpClient = new HttpClient();
+ _httpClient.DefaultRequestHeaders.Add("User-Agent", "LinksPlatform Bot/1.0");
+ }
+
+ ///
+ ///
+ /// Determines whether this instance should trigger based on the context.
+ ///
+ ///
+ ///
+ ///
+ /// The issue context.
+ ///
+ ///
+ ///
+ /// True if the issue contains gist or pastebin URLs, false otherwise.
+ ///
+ ///
+ public async Task Condition(TContext context)
+ {
+ var bodyContent = context.Body ?? "";
+ var titleContent = context.Title ?? "";
+ var fullContent = titleContent + " " + bodyContent;
+
+ // Check if the issue contains gist.github.com or pastebin URLs
+ var gistPattern = @"https?://gist\.github\.com/[^\s]+";
+ var pastebinPattern = @"https?://pastebin\.com/[^\s]+";
+
+ var hasGist = Regex.IsMatch(fullContent, gistPattern, RegexOptions.IgnoreCase);
+ var hasPastebin = Regex.IsMatch(fullContent, pastebinPattern, RegexOptions.IgnoreCase);
+
+ return hasGist || hasPastebin;
+ }
+
+ ///
+ ///
+ /// Executes the action when the condition is met.
+ ///
+ ///
+ ///
+ ///
+ /// The issue context.
+ ///
+ ///
+ public async Task Action(TContext context)
+ {
+ try
+ {
+ var bodyContent = context.Body ?? "";
+ var titleContent = context.Title ?? "";
+ var fullContent = titleContent + " " + bodyContent;
+
+ // Extract URLs
+ var urls = ExtractUrls(fullContent);
+
+ if (!urls.Any())
+ {
+ return;
+ }
+
+ // Process each URL and extract errors
+ var allErrors = new List();
+ foreach (var url in urls)
+ {
+ var errors = await ExtractErrorsFromUrl(url);
+ allErrors.AddRange(errors);
+ }
+
+ if (!allErrors.Any())
+ {
+ await _storage.CreateIssueComment(context.Repository.Id, context.Number,
+ "I found the gist/pastebin links you provided, but couldn't detect any clear error messages to search for. " +
+ "Could you please highlight the specific error you're encountering?");
+ return;
+ }
+
+ // Search for solutions for each error
+ var searchResults = new List();
+ foreach (var error in allErrors.Take(3)) // Limit to 3 errors to avoid spam
+ {
+ var results = await SearchGoogleForError(error);
+ searchResults.AddRange(results);
+ }
+
+ if (searchResults.Any())
+ {
+ var responseMessage = "I found some potential solutions for the errors in your gist/pastebin:\n\n";
+ var uniqueResults = searchResults.Distinct().Take(5); // Limit to 5 unique results
+
+ foreach (var result in uniqueResults)
+ {
+ responseMessage += $"• {result}\n";
+ }
+
+ responseMessage += "\nHope these resources help you resolve the issue!";
+
+ await _storage.CreateIssueComment(context.Repository.Id, context.Number, responseMessage);
+ }
+ else
+ {
+ await _storage.CreateIssueComment(context.Repository.Id, context.Number,
+ "I analyzed the errors in your gist/pastebin but couldn't find specific solutions on the whitelisted sites. " +
+ "You might want to try searching on Stack Overflow or the official documentation.");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error in GoogleSearchErrorTrigger: {ex.Message}");
+ // Don't comment on the issue if there's an internal error
+ }
+ }
+
+ ///
+ ///
+ /// Extracts gist and pastebin URLs from content.
+ ///
+ ///
+ ///
+ ///
+ /// The content to search.
+ ///
+ ///
+ ///
+ /// A list of extracted URLs.
+ ///
+ ///
+ private List ExtractUrls(string content)
+ {
+ var urls = new List();
+
+ // Extract gist URLs
+ var gistPattern = @"https?://gist\.github\.com/[^\s]+";
+ var gistMatches = Regex.Matches(content, gistPattern, RegexOptions.IgnoreCase);
+ urls.AddRange(gistMatches.Cast().Select(m => m.Value));
+
+ // Extract pastebin URLs
+ var pastebinPattern = @"https?://pastebin\.com/[^\s]+";
+ var pastebinMatches = Regex.Matches(content, pastebinPattern, RegexOptions.IgnoreCase);
+ urls.AddRange(pastebinMatches.Cast().Select(m => m.Value));
+
+ return urls;
+ }
+
+ ///
+ ///
+ /// Extracts error messages from a gist or pastebin URL.
+ ///
+ ///
+ ///
+ ///
+ /// The URL to process.
+ ///
+ ///
+ ///
+ /// A list of detected error messages.
+ ///
+ ///
+ private async Task> ExtractErrorsFromUrl(string url)
+ {
+ var errors = new List();
+
+ try
+ {
+ string content;
+
+ if (url.Contains("gist.github.com"))
+ {
+ content = await GetGistContent(url);
+ }
+ else if (url.Contains("pastebin.com"))
+ {
+ content = await GetPastebinContent(url);
+ }
+ else
+ {
+ return errors;
+ }
+
+ if (!string.IsNullOrEmpty(content))
+ {
+ errors.AddRange(ExtractErrorMessages(content));
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error extracting content from URL {url}: {ex.Message}");
+ }
+
+ return errors;
+ }
+
+ ///
+ ///
+ /// Gets the raw content from a GitHub gist.
+ ///
+ ///
+ ///
+ ///
+ /// The gist URL.
+ ///
+ ///
+ ///
+ /// The raw content of the gist.
+ ///
+ ///
+ private async Task GetGistContent(string gistUrl)
+ {
+ // Convert gist URL to raw URL
+ // From: https://gist.github.com/username/gist-id
+ // To: https://gist.githubusercontent.com/username/gist-id/raw/
+
+ var match = Regex.Match(gistUrl, @"https?://gist\.github\.com/([^/]+)/([^/?#]+)");
+ if (match.Success)
+ {
+ var username = match.Groups[1].Value;
+ var gistId = match.Groups[2].Value;
+ var rawUrl = $"https://gist.githubusercontent.com/{username}/{gistId}/raw/";
+
+ var response = await _httpClient.GetStringAsync(rawUrl);
+ return response;
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ ///
+ /// Gets the raw content from a Pastebin URL.
+ ///
+ ///
+ ///
+ ///
+ /// The pastebin URL.
+ ///
+ ///
+ ///
+ /// The raw content of the paste.
+ ///
+ ///
+ private async Task GetPastebinContent(string pastebinUrl)
+ {
+ // Convert pastebin URL to raw URL
+ // From: https://pastebin.com/paste-id
+ // To: https://pastebin.com/raw/paste-id
+
+ var match = Regex.Match(pastebinUrl, @"https?://pastebin\.com/([^/?#]+)");
+ if (match.Success)
+ {
+ var pasteId = match.Groups[1].Value;
+ var rawUrl = $"https://pastebin.com/raw/{pasteId}";
+
+ var response = await _httpClient.GetStringAsync(rawUrl);
+ return response;
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ ///
+ /// Extracts error messages from content using pattern matching.
+ ///
+ ///
+ ///
+ ///
+ /// The content to analyze.
+ ///
+ ///
+ ///
+ /// A list of detected error messages.
+ ///
+ ///
+ private List ExtractErrorMessages(string content)
+ {
+ var errors = new List();
+
+ // Common error patterns
+ var errorPatterns = new[]
+ {
+ // Exception patterns
+ @"(\w+Exception:?.+)",
+ @"(Error:?.+)",
+ @"(Fatal error:?.+)",
+ @"(Uncaught \w+:?.+)",
+
+ // Compilation errors
+ @"(error CS\d+:?.+)",
+ @"(error C\d+:?.+)",
+ @"(Build FAILED\..+)",
+
+ // Runtime errors
+ @"(Traceback \(most recent call last\):?.+)",
+ @"(at \w+\.\w+\(.+\))",
+
+ // HTTP errors
+ @"(\d{3} \w+:?.+)",
+
+ // Database errors
+ @"(SQL\w* error:?.+)",
+
+ // Generic error messages
+ @"(failed to .+)",
+ @"(cannot .+)",
+ @"(unable to .+)"
+ };
+
+ foreach (var pattern in errorPatterns)
+ {
+ var matches = Regex.Matches(content, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
+ foreach (Match match in matches)
+ {
+ var errorMessage = match.Groups[1].Value.Trim();
+ if (errorMessage.Length > 10 && errorMessage.Length < 500) // Reasonable length
+ {
+ errors.Add(errorMessage);
+ }
+ }
+ }
+
+ return errors.Distinct().ToList();
+ }
+
+ ///
+ ///
+ /// Searches Google for solutions to an error and returns whitelisted results.
+ ///
+ ///
+ ///
+ ///
+ /// The error message to search for.
+ ///
+ ///
+ ///
+ /// A list of whitelisted search result URLs.
+ ///
+ ///
+ private async Task> SearchGoogleForError(string errorMessage)
+ {
+ var results = new List();
+
+ try
+ {
+ // Use a simple site-specific search approach since Google Custom Search API requires API key
+ foreach (var domain in _whitelistedDomains.Take(3)) // Limit to avoid being blocked
+ {
+ var searchQuery = $"site:{domain} {WebUtility.UrlEncode(errorMessage)}";
+ var searchUrl = $"https://www.google.com/search?q={searchQuery}";
+
+ // For a production implementation, you would:
+ // 1. Use Google Custom Search API with an API key
+ // 2. Parse the JSON response
+ // 3. Extract actual URLs
+
+ // For now, we'll create helpful search links
+ results.Add($"Search on {domain}: https://www.google.com/search?q=site:{domain}+{WebUtility.UrlEncode(errorMessage.Substring(0, Math.Min(errorMessage.Length, 100)))}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error searching for: {errorMessage}. Exception: {ex.Message}");
+ }
+
+ return results;
+ }
+
+ ///
+ ///
+ /// Disposes of resources.
+ ///
+ ///
+ ///
+ public void Dispose()
+ {
+ _httpClient?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/test-gist-error.md b/examples/test-gist-error.md
new file mode 100644
index 00000000..ed585b45
--- /dev/null
+++ b/examples/test-gist-error.md
@@ -0,0 +1,21 @@
+# Example Issue with Gist Error
+
+This is an example of how the GoogleSearchErrorTrigger would work.
+
+Here's my error in a gist: https://gist.github.com/example/abc123
+
+The expected behavior:
+1. The bot detects the gist URL in the issue
+2. Extracts error messages from the gist content
+3. Searches Google for solutions on whitelisted sites
+4. Replies with helpful search links
+
+## Sample gist content that would trigger the bot:
+
+```
+System.NullReferenceException: Object reference not set to an instance of an object
+ at MyApp.Controllers.HomeController.Index() in HomeController.cs:line 25
+ at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.Execute()
+```
+
+The bot would detect the "System.NullReferenceException" and provide search links for Stack Overflow, docs.microsoft.com, etc.
\ No newline at end of file
diff --git a/examples/test-pastebin-error.md b/examples/test-pastebin-error.md
new file mode 100644
index 00000000..7fb884b5
--- /dev/null
+++ b/examples/test-pastebin-error.md
@@ -0,0 +1,25 @@
+# Example Issue with Pastebin Error
+
+Another example showing pastebin support:
+
+Here's the stack trace: https://pastebin.com/xyz789
+
+## Sample pastebin content that would trigger the bot:
+
+```
+TypeError: Cannot read properties of undefined (reading 'length')
+ at processArray (/app/src/utils.js:15:20)
+ at main (/app/src/index.js:8:5)
+ at Object. (/app/src/index.js:25:1)
+```
+
+The bot would detect the JavaScript "TypeError" and provide relevant search links for JavaScript debugging resources.
+
+## Error patterns the bot can detect:
+
+- Exception types (NullReferenceException, TypeError, etc.)
+- Compilation errors (error CS1234, error C2065)
+- Runtime errors (Uncaught Exception)
+- HTTP errors (404 Not Found, 500 Internal Server Error)
+- Build failures (Build FAILED)
+- Database errors (SQL error)
\ No newline at end of file