Skip to content
Merged
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
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,
}
}
152 changes: 152 additions & 0 deletions src/RandomAPI/APIServices/Services/BaseWebhookService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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)
{
string safeUrlForLog = url;
if (string.IsNullOrWhiteSpace(url))
{
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);
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