Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion csharp/Platform.Bot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private static async Task<int> 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 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), new AddUserLinkTrigger(githubStorage, dbContext), new RemoveUserLinkTrigger(githubStorage, dbContext), new ListUserLinksTrigger(githubStorage, dbContext));
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();
Expand Down
140 changes: 140 additions & 0 deletions csharp/Platform.Bot/Triggers/AddUserLinkTrigger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Interfaces;
using Octokit;
using Storage.Local;
using Storage.Remote.GitHub;

namespace Platform.Bot.Triggers
{
using TContext = Issue;

/// <summary>
/// <para>
/// Represents the add user link trigger for handling user link addition commands.
/// </para>
/// <para></para>
/// </summary>
/// <seealso cref="ITrigger{TContext}"/>
internal class AddUserLinkTrigger : ITrigger<TContext>
{
private readonly GitHubStorage _storage;
private readonly FileStorage _fileStorage;
private readonly Regex _commandPattern;

/// <summary>
/// <para>
/// Initializes a new <see cref="AddUserLinkTrigger"/> instance.
/// </para>
/// <para></para>
/// </summary>
/// <param name="storage">
/// <para>The GitHub storage.</para>
/// <para></para>
/// </param>
/// <param name="fileStorage">
/// <para>The file storage.</para>
/// <para></para>
/// </param>
public AddUserLinkTrigger(GitHubStorage storage, FileStorage fileStorage)
{
_storage = storage;
_fileStorage = fileStorage;
_commandPattern = new Regex(@"^add link (\w+) (.+)$", RegexOptions.IgnoreCase);
}

/// <summary>
/// <para>
/// Determines whether this instance condition matches the context.
/// </para>
/// <para></para>
/// </summary>
/// <param name="context">
/// <para>The issue context.</para>
/// <para></para>
/// </param>
/// <returns>
/// <para>True if the issue title matches the add link pattern.</para>
/// <para></para>
/// </returns>
public async Task<bool> Condition(TContext context)
{
return _commandPattern.IsMatch(context.Title.Trim());
}

/// <summary>
/// <para>
/// Executes the add user link action.
/// </para>
/// <para></para>
/// </summary>
/// <param name="context">
/// <para>The issue context.</para>
/// <para></para>
/// </param>
public async Task Action(TContext context)
{
var match = _commandPattern.Match(context.Title.Trim());
if (!match.Success)
{
await _storage.CreateIssueComment(context.Repository.Id, context.Number, "❌ Invalid command format. Use: `add link <platform> <url>`");
_storage.CloseIssue(context);
return;
}

var platform = match.Groups[1].Value;
var url = match.Groups[2].Value;
var username = context.User.Login;

// Validate platform is supported
if (!DomainWhitelist.GetSupportedPlatforms().Any(p => string.Equals(p, platform, StringComparison.OrdinalIgnoreCase)))
{
var supportedPlatforms = string.Join(", ", DomainWhitelist.GetSupportedPlatforms());
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
$"❌ Platform '{platform}' is not supported. Supported platforms: {supportedPlatforms}");
_storage.CloseIssue(context);
return;
}

// Validate URL is allowed for the platform
if (!DomainWhitelist.IsUrlAllowed(platform, url))
{
var allowedDomains = string.Join(", ", DomainWhitelist.GetAllowedDomains(platform));
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
$"❌ URL '{url}' is not allowed for platform '{platform}'. Allowed domains: {allowedDomains}");
_storage.CloseIssue(context);
return;
}

try
{
// Check if user already has a link for this platform
var existingLinks = _fileStorage.GetUserLinks(username);
var existingLink = existingLinks.Find(link => string.Equals(link.Platform, platform, StringComparison.OrdinalIgnoreCase));

if (existingLink != null)
{
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
$"❌ You already have a {platform} link: {existingLink.Url}. Remove it first if you want to change it.");
_storage.CloseIssue(context);
return;
}

// Add the user link
_fileStorage.AddUserLink(username, platform, url);

await _storage.CreateIssueComment(context.Repository.Id, context.Number,
$"✅ Successfully added {platform} link for @{username}: {url}");
}
catch (Exception ex)
{
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
$"❌ Error adding link: {ex.Message}");
}

_storage.CloseIssue(context);
}
}
}
144 changes: 144 additions & 0 deletions csharp/Platform.Bot/Triggers/ListUserLinksTrigger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Interfaces;
using Octokit;
using Storage.Local;
using Storage.Remote.GitHub;

namespace Platform.Bot.Triggers
{
using TContext = Issue;

/// <summary>
/// <para>
/// Represents the list user links trigger for displaying user links.
/// </para>
/// <para></para>
/// </summary>
/// <seealso cref="ITrigger{TContext}"/>
internal class ListUserLinksTrigger : ITrigger<TContext>
{
private readonly GitHubStorage _storage;
private readonly FileStorage _fileStorage;
private readonly Regex _listMyLinksPattern;
private readonly Regex _listUserLinksPattern;

/// <summary>
/// <para>
/// Initializes a new <see cref="ListUserLinksTrigger"/> instance.
/// </para>
/// <para></para>
/// </summary>
/// <param name="storage">
/// <para>The GitHub storage.</para>
/// <para></para>
/// </param>
/// <param name="fileStorage">
/// <para>The file storage.</para>
/// <para></para>
/// </param>
public ListUserLinksTrigger(GitHubStorage storage, FileStorage fileStorage)
{
_storage = storage;
_fileStorage = fileStorage;
_listMyLinksPattern = new Regex(@"^(list my links|my links)$", RegexOptions.IgnoreCase);
_listUserLinksPattern = new Regex(@"^list links (\w+)$", RegexOptions.IgnoreCase);
}

/// <summary>
/// <para>
/// Determines whether this instance condition matches the context.
/// </para>
/// <para></para>
/// </summary>
/// <param name="context">
/// <para>The issue context.</para>
/// <para></para>
/// </param>
/// <returns>
/// <para>True if the issue title matches any list links pattern.</para>
/// <para></para>
/// </returns>
public async Task<bool> Condition(TContext context)
{
var title = context.Title.Trim();
return _listMyLinksPattern.IsMatch(title) || _listUserLinksPattern.IsMatch(title);
}

/// <summary>
/// <para>
/// Executes the list user links action.
/// </para>
/// <para></para>
/// </summary>
/// <param name="context">
/// <para>The issue context.</para>
/// <para></para>
/// </param>
public async Task Action(TContext context)
{
var title = context.Title.Trim();
var targetUsername = context.User.Login; // Default to the requesting user
var isRequestingOwnLinks = true;

// Check if requesting links for a specific user
var userMatch = _listUserLinksPattern.Match(title);
if (userMatch.Success)
{
targetUsername = userMatch.Groups[1].Value;
isRequestingOwnLinks = string.Equals(targetUsername, context.User.Login, StringComparison.OrdinalIgnoreCase);
}

try
{
var userLinks = _fileStorage.GetUserLinks(targetUsername);

if (!userLinks.Any())
{
var message = isRequestingOwnLinks
? "You don't have any links registered."
: $"User @{targetUsername} doesn't have any links registered.";

await _storage.CreateIssueComment(context.Repository.Id, context.Number, message);
}
else
{
var messageBuilder = new StringBuilder();
var displayUsername = isRequestingOwnLinks ? "Your" : $"@{targetUsername}'s";
messageBuilder.AppendLine($"## {displayUsername} Links\n");

foreach (var link in userLinks.OrderBy(l => l.Platform))
{
messageBuilder.AppendLine($"**{link.Platform}**: {link.Url}");
}

messageBuilder.AppendLine($"\n*Total: {userLinks.Count} link{(userLinks.Count == 1 ? "" : "s")}*");

if (isRequestingOwnLinks)
{
messageBuilder.AppendLine("\n---");
messageBuilder.AppendLine("**Commands:**");
messageBuilder.AppendLine("- `add link <platform> <url>` - Add a new link");
messageBuilder.AppendLine("- `remove link <platform>` - Remove a link");
messageBuilder.AppendLine("- `my links` - View your links");

var supportedPlatforms = string.Join(", ", DomainWhitelist.GetSupportedPlatforms());
messageBuilder.AppendLine($"\n**Supported platforms:** {supportedPlatforms}");
}

await _storage.CreateIssueComment(context.Repository.Id, context.Number, messageBuilder.ToString());
}
}
catch (Exception ex)
{
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
$"❌ Error retrieving links: {ex.Message}");
}

_storage.CloseIssue(context);
}
}
}
Loading
Loading