Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions .github/workflows/CodeQL_PR_Analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ jobs:
dotnet-version: '8.0.x'

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: csharp

- name: Build Project
run: dotnet build src/RandomAPI/RandomAPI.csproj

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
30 changes: 27 additions & 3 deletions src/RandomAPI/APIServices/ServiceInterfaces/IWebhookService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using RandomAPI.Models;
using RandomAPI.Services.Webhooks;

public interface IWebhookService
{
/// <summary>
Expand All @@ -9,16 +13,36 @@ public interface IWebhookService
/// Registers a new webhook listener URL.
/// </summary>
/// <returns>True if added, false if it already existed.</returns>
bool AddListener(string url);
Task AddListenerAsync(string url, WebhookType type = default);

/// <summary>
/// Removes a webhook listener URL.
/// </summary>
/// <returns>True if removed, false if not found.</returns>
bool RemoveListener(string url);
Task<bool> RemoveListenerAsync(string url);

/// <summary>
/// Returns a snapshot of all registered listener URLs.
/// </summary>
IEnumerable<string> GetListeners();
Task<IEnumerable<string>> GetListenersAsync();

/// <summary>
/// returns a snapshot of all registered listenrs of a given type
/// </summary>
/// <param name="type"> the type of url</param>

Task<IEnumerable<string>> GetListenersAsync(WebhookType type = WebhookType.Default);

// Controller Logic Methods (Implemented in the derived class)
public Task<IActionResult> HandleGetListenersActionAsync();
public Task<IActionResult> HandleGetListenersOfTypeAsync(WebhookType type);
public Task<IActionResult> HandleRegisterActionAsync(string url, WebhookType type = default);
public Task<IActionResult> HandleUnregisterActionAsync(string url);
public Task<IActionResult> HandleBroadcastActionAsync(IWebHookPayload payload);

public enum WebhookType
{
Default = 0,
Discord = 1,
}
}
151 changes: 151 additions & 0 deletions src/RandomAPI/APIServices/Services/BaseWebhookService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
using Microsoft.AspNetCore.Mvc;
using RandomAPI.Models;
using RandomAPI.Repository;
using static IWebhookService;

namespace RandomAPI.Services.Webhooks
{
public class WebhookActionService : BaseWebhookService, IWebhookService
{

public WebhookActionService(IWebhookRepository repo, ILogger<IWebhookService> logger)
: base(repo, logger) { }

public async Task<IActionResult> HandleGetListenersActionAsync()
{
var urls = await base.GetListenersAsync();
return new OkObjectResult(urls);
}

public async Task<IActionResult> HandleGetListenersOfTypeAsync(WebhookType type)
{
var urls = await base.GetListenersAsync(type);
return new OkObjectResult(urls);
}

public async Task<IActionResult> HandleRegisterActionAsync([FromBody] string url, IWebhookService.WebhookType type = default)
{
if (string.IsNullOrWhiteSpace(url))
return new BadRequestObjectResult("URL cannot be empty.");
//neede both on regisdter and deregister
url = url.Trim();
var safeUrlForLog = url.Replace("\r", "").Replace("\n", "");

await base.AddListenerAsync(url, type);

_logger.LogInformation("Registered new webhook listener: {Url}", safeUrlForLog);

return new OkObjectResult(new { Message = $"Listener added successfully: {url}" });
}

public async Task<IActionResult> HandleUnregisterActionAsync([FromBody] string url)
{
if (string.IsNullOrWhiteSpace(url))
{
var safeUrlForLog = url.Replace("\r", "").Replace("\n", "");
return new BadRequestObjectResult("URL cannot be empty.");
}
url = url.Trim();

var removed = await base.RemoveListenerAsync(url);

if (!removed)
{
return new NotFoundObjectResult(new { Message = $"URL not found: {url}" });
}

_logger.LogInformation("Unregistered webhook listener: {Url}", safeUrlForLog);

Check failure on line 57 in src/RandomAPI/APIServices/Services/BaseWebhookService.cs

View workflow job for this annotation

GitHub Actions / Analyze code with CodeQL

The name 'safeUrlForLog' does not exist in the current context

Check failure on line 57 in src/RandomAPI/APIServices/Services/BaseWebhookService.cs

View workflow job for this annotation

GitHub Actions / Analyze code with CodeQL

The name 'safeUrlForLog' does not exist in the current context
Comment thread Dismissed
return new OkObjectResult(new { Message = $"Listener removed: {url}" });
}

public async Task<IActionResult> HandleBroadcastActionAsync([FromBody] IWebHookPayload payload)
{
var listeners = await base.GetListenersAsync();

if (!listeners.Any())
return new BadRequestObjectResult("No listeners registered to broadcast to.");

switch (payload)
{
case WebhookPayload p:
p.Timestamp = DateTime.UtcNow;

break;

case DiscordWebhookPayload p:
break;

default:
_logger.LogWarning("Received unsupported payload type: {Type}", payload.GetType().Name);
return new BadRequestObjectResult(new { Message = "Unsupported webhook payload type." });
}

_logger.LogInformation("Broadcasting test payload: {Message}", payload.content);
await base.BroadcastAsync(payload);
return new OkObjectResult(new
{
Message = $"Broadcast sent for message: '{payload.content}'. Check logs for delivery status."
});
}
}


public class BaseWebhookService
{
protected readonly IWebhookRepository _repo;
protected readonly HttpClient _client = new();
protected readonly ILogger<IWebhookService> _logger;

public BaseWebhookService(IWebhookRepository repo, ILogger<IWebhookService> logger)
{
_repo = repo;
_logger = logger;
}

public async Task<IEnumerable<string>> GetListenersAsync()
{
var urls = await _repo.GetAllUrlsAsync();
return urls.Select(u => u.Url);
}

public async Task<IEnumerable<string>> GetListenersAsync(WebhookType type = WebhookType.Default)
{
var urls = await _repo.GetUrlsOfTypeAsync(type);
return urls.Select(u => u.Url);
}

public async Task AddListenerAsync(string url, WebhookType type = default)
{
await _repo.AddUrlAsync(url, type);
}

public async Task<bool> RemoveListenerAsync(string url)
{
var result = await _repo.DeleteUrlAsync(url);
return result > 0;
}

//basic broadcast for all
public async Task BroadcastAsync<T>(T payload) where T : class
{
IEnumerable<string> urls = await GetListenersAsync();
await BroadcastAsync(payload, urls);
}
//derived for the payloads
public async Task BroadcastAsync<T>(T payload, IEnumerable<string> urls) where T : class
{
var tasks = urls.Select(async url =>
{
try
{
await _client.PostAsJsonAsync(url, payload);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Webhook POST failed for URL: {url}", url);
}
});
await Task.WhenAll(tasks);
}
}
}
4 changes: 2 additions & 2 deletions src/RandomAPI/APIServices/Services/DatabaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void Execute(Action<SqliteConnection> action)
/// <summary>
/// Opens a connection, executes a Dapper query, and returns a collection of results.
/// </summary>
/// <typeparam name="T">The type of object to map the result rows to (e.g., Event).</typeparam>
/// <typeparam name="T">The Type of object to map the result rows to (e.g., Event).</typeparam>
/// <param name="sql">The SQL SELECT statement to execute.</param>
/// <param name="param">Optional parameters object for Dapper.</param>
/// <returns>A collection of mapped objects.</returns>
Expand Down Expand Up @@ -55,7 +55,7 @@ public async Task<int> ExecuteAsync(string sql, object? param = null)
/// <summary>
/// Opens a connection, executes a command, and returns a single value (e.g., the last inserted ID).
/// </summary>
/// <typeparam name="T">The type of the scalar result (e.g., int).</typeparam>
/// <typeparam name="T">The Type of the scalar result (e.g., int).</typeparam>
/// <param name="sql">The SQL command to execute.</param>
/// <param name="param">Optional parameters object for Dapper.</param>
/// <returns>The single scalar result.</returns>
Expand Down
5 changes: 4 additions & 1 deletion src/RandomAPI/APIServices/Services/WebhookPayload.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using RandomAPI.Models;
using RandomAPI.Services.Webhooks;

namespace RandomAPI.Services.Webhooks
{
public class WebhookPayload : ICustomWebhookPayload
Expand All @@ -10,4 +13,4 @@ public class DiscordWebhookPayload : IWebHookPayload
{
public string content { get; set; } = "";
}
}
}
41 changes: 0 additions & 41 deletions src/RandomAPI/APIServices/Services/WebhookService.cs

This file was deleted.

Loading
Loading