From 16e07fcc1eb80d9a0ff857e5489c2da353c3271d Mon Sep 17 00:00:00 2001
From: Skyfall1235 <113722636+Skyfall1235@users.noreply.github.com>
Date: Thu, 4 Dec 2025 01:34:58 -0500
Subject: [PATCH 1/6] split webhook service into smaller components + add new
column for types of broadcasts
---
.../ServiceInterfaces/IWebhookService.cs | 30 +++-
.../Services/BaseWebhookService.cs | 149 ++++++++++++++++++
.../APIServices/Services/WebhookPayload.cs | 5 +-
.../APIServices/Services/WebhookService.cs | 41 -----
.../Controllers/WebhookController.cs | 97 +++++-------
.../ICustomWebhookPayload.cs | 2 +-
src/RandomAPI/Models/WebhookUrl.cs | 4 +-
src/RandomAPI/PersonalDev.db | Bin 16384 -> 24576 bytes
src/RandomAPI/Program.cs | 15 +-
.../Repository/IWebhookRepository.cs | 3 +-
src/RandomAPI/Repository/WebhookRepository.cs | 63 ++++++--
11 files changed, 283 insertions(+), 126 deletions(-)
create mode 100644 src/RandomAPI/APIServices/Services/BaseWebhookService.cs
delete mode 100644 src/RandomAPI/APIServices/Services/WebhookService.cs
rename src/RandomAPI/{APIServices/Services => Models}/ICustomWebhookPayload.cs (84%)
diff --git a/src/RandomAPI/APIServices/ServiceInterfaces/IWebhookService.cs b/src/RandomAPI/APIServices/ServiceInterfaces/IWebhookService.cs
index 4eac03a..b578d0f 100644
--- a/src/RandomAPI/APIServices/ServiceInterfaces/IWebhookService.cs
+++ b/src/RandomAPI/APIServices/ServiceInterfaces/IWebhookService.cs
@@ -1,3 +1,7 @@
+using Microsoft.AspNetCore.Mvc;
+using RandomAPI.Models;
+using RandomAPI.Services.Webhooks;
+
public interface IWebhookService
{
///
@@ -9,16 +13,36 @@ public interface IWebhookService
/// Registers a new webhook listener URL.
///
/// True if added, false if it already existed.
- bool AddListener(string url);
+ Task AddListenerAsync(string url, WebhookType type = default);
///
/// Removes a webhook listener URL.
///
/// True if removed, false if not found.
- bool RemoveListener(string url);
+ Task RemoveListenerAsync(string url);
///
/// Returns a snapshot of all registered listener URLs.
///
- IEnumerable GetListeners();
+ Task> GetListenersAsync();
+
+ ///
+ /// returns a snapshot of all registered listenrs of a given type
+ ///
+ /// the type of url
+
+ Task> GetListenersAsync(WebhookType type = WebhookType.Default);
+
+ // Controller Logic Methods (Implemented in the derived class)
+ public Task HandleGetListenersActionAsync();
+ public Task HandleGetListenersOfTypeAsync(WebhookType type);
+ public Task HandleRegisterActionAsync(string url, WebhookType type = default);
+ public Task HandleUnregisterActionAsync(string url);
+ public Task HandleBroadcastActionAsync(IWebHookPayload payload);
+
+ public enum WebhookType
+ {
+ Default = 0,
+ Discord = 1,
+ }
}
diff --git a/src/RandomAPI/APIServices/Services/BaseWebhookService.cs b/src/RandomAPI/APIServices/Services/BaseWebhookService.cs
new file mode 100644
index 0000000..3c9fe52
--- /dev/null
+++ b/src/RandomAPI/APIServices/Services/BaseWebhookService.cs
@@ -0,0 +1,149 @@
+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 logger)
+ : base(repo, logger) { }
+
+ public async Task HandleGetListenersActionAsync()
+ {
+ var urls = await base.GetListenersAsync();
+ return new OkObjectResult(urls);
+ }
+
+ public async Task HandleGetListenersOfTypeAsync(WebhookType type)
+ {
+ var urls = await base.GetListenersAsync(type);
+ return new OkObjectResult(urls);
+ }
+
+ public async Task 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();
+
+ await base.AddListenerAsync(url, type);
+
+ _logger.LogInformation("Registered new webhook listener: {Url}", url);
+
+ return new OkObjectResult(new { Message = $"Listener added successfully: {url}" });
+ }
+
+ public async Task HandleUnregisterActionAsync([FromBody] string url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ 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}", url);
+ return new OkObjectResult(new { Message = $"Listener removed: {url}" });
+ }
+
+ public async Task 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 _logger;
+
+ public BaseWebhookService(IWebhookRepository repo, ILogger logger)
+ {
+ _repo = repo;
+ _logger = logger;
+ }
+
+ public async Task> GetListenersAsync()
+ {
+ var urls = await _repo.GetAllUrlsAsync();
+ return urls.Select(u => u.Url);
+ }
+
+ public async Task> 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 RemoveListenerAsync(string url)
+ {
+ var result = await _repo.DeleteUrlAsync(url);
+ return result > 0;
+ }
+
+ //basic broadcast for all
+ public async Task BroadcastAsync(T payload) where T : class
+ {
+ IEnumerable urls = await GetListenersAsync();
+ await BroadcastAsync(payload, urls);
+ }
+ //derived for the payloads
+ public async Task BroadcastAsync(T payload, IEnumerable 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RandomAPI/APIServices/Services/WebhookPayload.cs b/src/RandomAPI/APIServices/Services/WebhookPayload.cs
index cf36fe2..27489dc 100644
--- a/src/RandomAPI/APIServices/Services/WebhookPayload.cs
+++ b/src/RandomAPI/APIServices/Services/WebhookPayload.cs
@@ -1,3 +1,6 @@
+using RandomAPI.Models;
+using RandomAPI.Services.Webhooks;
+
namespace RandomAPI.Services.Webhooks
{
public class WebhookPayload : ICustomWebhookPayload
@@ -10,4 +13,4 @@ public class DiscordWebhookPayload : IWebHookPayload
{
public string content { get; set; } = "";
}
-}
+}
\ No newline at end of file
diff --git a/src/RandomAPI/APIServices/Services/WebhookService.cs b/src/RandomAPI/APIServices/Services/WebhookService.cs
deleted file mode 100644
index 2844a17..0000000
--- a/src/RandomAPI/APIServices/Services/WebhookService.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Collections.Concurrent;
-
-namespace RandomAPI.Services.Webhooks
-{
- public class WebhookService : IWebhookService
- {
- private readonly ConcurrentDictionary _webhookUrls = new();
- private readonly HttpClient _client = new();
- private readonly ILogger _logger;
-
- public WebhookService(ILogger logger)
- {
- _logger = logger;
- }
-
- public IEnumerable GetListeners() => _webhookUrls.Keys;
-
- public bool AddListener(string url) => _webhookUrls.TryAdd(url, 0);
-
- public bool RemoveListener(string url) => _webhookUrls.TryRemove(url, out _);
-
- public async Task BroadcastAsync(T payload) where T : class
- {
- // Snapshot-safe enumeration
- var tasks = _webhookUrls.Keys.Select(async url =>
- {
- try
- {
- await _client.PostAsJsonAsync(url, payload);
- }
- catch (Exception ex)
- {
- {
- _logger.LogWarning(ex, "WebhookPayload failed to Post");
- }
- }
- });
- await Task.WhenAll(tasks);
- }
- }
-}
diff --git a/src/RandomAPI/Controllers/WebhookController.cs b/src/RandomAPI/Controllers/WebhookController.cs
index 2c9d8a7..4744884 100644
--- a/src/RandomAPI/Controllers/WebhookController.cs
+++ b/src/RandomAPI/Controllers/WebhookController.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using RandomAPI.Services.Webhooks;
+using static IWebhookService;
namespace RandomAPI.Controllers
{
@@ -10,7 +11,9 @@ public class WebhookController : ControllerBase
private readonly ILogger _logger;
private readonly IWebhookService _webhookService;
- public WebhookController(ILogger logger, IWebhookService webhookService)
+ public WebhookController(
+ ILogger logger,
+ IWebhookService webhookService)
{
_logger = logger;
_webhookService = webhookService;
@@ -20,105 +23,77 @@ public WebhookController(ILogger logger, IWebhookService webh
/// Gets a list of all currently registered webhook listener URLs.
///
[HttpGet("listeners")]
- public IActionResult GetListeners()
+ public async Task GetListeners()
{
- return Ok(_webhookService.GetListeners());
+ var urls = await _webhookService.GetListenersAsync();
+ return Ok(urls);
}
///
/// Registers a new URL to receive webhook payloads.
///
- /// The URL to register.
[HttpPost("register")]
- public IActionResult Register([FromBody] string url)
+ public async Task RegisterUrl([FromBody] string url)
{
- if (string.IsNullOrWhiteSpace(url))
- {
- return BadRequest("URL cannot be empty.");
- }
-
- if (_webhookService.AddListener(url))
- {
- _logger.LogInformation("Registered new webhook listener: {Url}", url);
- return Ok(new { Message = $"Listener added successfully for {url}" });
- }
-
- return Conflict(new { Message = $"URL is already registered: {url}" });
+ return await _webhookService.HandleRegisterActionAsync(url);
+ }
+ ///
+ /// Registers a new URL to receive webhook payloads.
+ ///
+ [HttpPost("register-discord")]
+ public async Task RegisterDiscordUrl([FromBody] string url)
+ {
+ return await _webhookService.HandleRegisterActionAsync(url, WebhookType.Discord);
}
///
/// Removes a URL from the list of webhook listeners.
///
- /// The URL to unregister.
[HttpDelete("unregister")]
- public IActionResult Unregister([FromBody] string url)
+ public async Task UnregisterUrl([FromBody] string url)
{
- if (string.IsNullOrWhiteSpace(url))
- {
- return BadRequest("URL cannot be empty.");
- }
-
- if (_webhookService.RemoveListener(url))
- {
- _logger.LogInformation("Unregistered webhook listener: {Url}", url);
- return Ok(new { Message = $"Listener removed successfully for {url}" });
- }
-
- return NotFound(new { Message = $"URL not found in listener list: {url}" });
+ return await _webhookService.HandleUnregisterActionAsync(url);
}
///
- /// Endpoint to manually trigger a test broadcast of a payload.
+ /// Endpoint to manually trigger a test broadcast.
///
- /// The payload message to send.
[HttpPost("debug/broadcast-test")]
public async Task BroadcastTest([FromBody] WebhookPayload payload)
{
- if (!_webhookService.GetListeners().Any())
- {
+ var listeners = await _webhookService.GetListenersAsync();
+
+ if (!listeners.Any())
return BadRequest("No listeners registered to broadcast to.");
- }
- // Ensure the payload has a fresh timestamp
payload.Timestamp = DateTime.UtcNow;
_logger.LogInformation("Broadcasting test payload: {Message}", payload.content);
- // This runs asynchronously in the background. We don't wait for every success.
await _webhookService.BroadcastAsync(payload);
- return Ok(
- new
- {
- Message = $"Broadcast initiated successfully for message: '{payload.content}'. Check logs for delivery status.",
- }
- );
+ return Ok(new
+ {
+ Message = $"Broadcast sent for message: '{payload.content}'. Check logs for delivery status."
+ });
}
[HttpPost("debug/discord-broadcast-test")]
- public async Task BroadcastDiscordTest(
- [FromBody] DiscordWebhookPayload payload
- )
+ public async Task BroadcastDiscordTest([FromBody] DiscordWebhookPayload payload)
{
- if (!_webhookService.GetListeners().Any())
- {
+ var listeners = await _webhookService.GetListenersAsync();
+
+ if (!listeners.Any())
return BadRequest("No listeners registered to broadcast to.");
- }
- // Ensure the payload has a fresh timestamp
- //payload.Timestamp = DateTime.UtcNow;
+ _logger.LogInformation("Broadcasting Discord payload: {Message}", payload.content);
- _logger.LogInformation("Broadcasting test payload: {Message}", payload.content);
-
- // This runs asynchronously in the background. We don't wait for every success.
await _webhookService.BroadcastAsync(payload);
- return Ok(
- new
- {
- Message = $"Broadcast initiated successfully for message: '{payload.content}'. Check logs for delivery status.",
- }
- );
+ return Ok(new
+ {
+ Message = $"Broadcast sent for message: '{payload.content}'. Check logs for delivery status."
+ });
}
}
}
diff --git a/src/RandomAPI/APIServices/Services/ICustomWebhookPayload.cs b/src/RandomAPI/Models/ICustomWebhookPayload.cs
similarity index 84%
rename from src/RandomAPI/APIServices/Services/ICustomWebhookPayload.cs
rename to src/RandomAPI/Models/ICustomWebhookPayload.cs
index 30efdac..3493eca 100644
--- a/src/RandomAPI/APIServices/Services/ICustomWebhookPayload.cs
+++ b/src/RandomAPI/Models/ICustomWebhookPayload.cs
@@ -1,4 +1,4 @@
-namespace RandomAPI.Services.Webhooks
+namespace RandomAPI.Models
{
public interface ICustomWebhookPayload : IWebHookPayload
{
diff --git a/src/RandomAPI/Models/WebhookUrl.cs b/src/RandomAPI/Models/WebhookUrl.cs
index ddd799f..49c1940 100644
--- a/src/RandomAPI/Models/WebhookUrl.cs
+++ b/src/RandomAPI/Models/WebhookUrl.cs
@@ -1,4 +1,5 @@
-namespace RandomAPI.Models
+
+namespace RandomAPI.Models
{
///
/// Represents a registered webhook listener URL stored in the database.
@@ -7,5 +8,6 @@ public class WebhookUrl
{
public int Id { get; set; }
public required string Url { get; set; }
+ public IWebhookService.WebhookType Type { get; set; }
}
}
diff --git a/src/RandomAPI/PersonalDev.db b/src/RandomAPI/PersonalDev.db
index 7a824e9a6326ca37137f93b51808bee04e1fcea5..3d02038a2458c89ca691997b315c515279152add 100644
GIT binary patch
literal 24576
zcmeI)%}?5390%~0BBBxN#Sm{#!bPGJ3JSQ(R==qo;?x{P)6`2s
zD2keLHlMQ%660jNgMxFUhw_hQr>K?B;Q)I}P0ZX-vnlp#;M>ei;LBs-I1d2`KmY;|
zfB*y_009U<;BN)4MDJuU6r!(dwsO$a_OyeBX&%a*rZtqENS6d&5{SfaWCb#Gfh-25
z$wdEbXYPwQAZ((fdGSl|5du^F5
zH^gn}sb+d0cjJF5E}a*5r4sMx5V=?#)ap+gvUpbx|C~VKC66!oV&Sp5(SHtAu77mb
z@_zSS6>xWR?hXk85P$##AOHafKmY;|fB*y_0D*B8n05z-_5Zj&UW^L@5P$##AOHaf
zKmY;|fB*y_&=YV!|EJmSl=DJ@00bZa0SG_<0uX=z1Rwwb2tZ&w1X!90(L=usVchk<
zhxJhG73-P39gjg`6cB&_1Rwwb2tWV=5P$##AOL}ZfS>VF^Zt6bc6j0OQdgG^+iqLW
zBN0`%YGy}WshLJZY3q@b{$EB|5pH#LEfG(|I4+vtV(W1(k&I}K_|~qG@3sqarL3ln
zq?+5P#kWhEvQyxj#q@stTRqLFBle;3bplX*cBPS@(XSW-_q@n$Y-6jhTmH(u>kqI|Sb?(8%;b2D)+
z3j4=0w^dJVr}%u~Y>O|)IQRSi9+silYnE{iNDzPk1Rwwb2tWV=5P$##AOL|e6Yw+h
cw7dRiK6tM#>HoI=_gpyLPyKfH|9buZ15=ca8vp(_ => new SqliteConnection(
- DBInitialization.CONNECTIONSTRING
-));
+builder.Services.AddScoped(sp =>
+{
+ var conn = new SqliteConnection("Data Source=PersonalDev.db;Cache=Shared");
+ conn.Open();
+ return conn;
+});
+builder.Services.AddScoped>(sp =>
+ () => sp.GetRequiredService());
+
#region Add Services
//webhook
-builder.Services.AddSingleton();
+builder.Services.AddScoped();
+
//time clock service
builder.Services.AddSingleton();
diff --git a/src/RandomAPI/Repository/IWebhookRepository.cs b/src/RandomAPI/Repository/IWebhookRepository.cs
index 511da29..9e9cbd7 100644
--- a/src/RandomAPI/Repository/IWebhookRepository.cs
+++ b/src/RandomAPI/Repository/IWebhookRepository.cs
@@ -8,7 +8,8 @@ namespace RandomAPI.Repository
public interface IWebhookRepository
{
Task> GetAllUrlsAsync();
- Task AddUrlAsync(string url);
+ Task> GetUrlsOfTypeAsync(IWebhookService.WebhookType type);
+ Task AddUrlAsync(string url, IWebhookService.WebhookType type);
Task DeleteUrlAsync(string url);
}
}
diff --git a/src/RandomAPI/Repository/WebhookRepository.cs b/src/RandomAPI/Repository/WebhookRepository.cs
index 7e0f4d7..5fa1d0a 100644
--- a/src/RandomAPI/Repository/WebhookRepository.cs
+++ b/src/RandomAPI/Repository/WebhookRepository.cs
@@ -1,4 +1,5 @@
using Dapper;
+using Microsoft.Data.Sqlite;
using RandomAPI.Models;
using System.Data;
@@ -10,11 +11,18 @@ namespace RandomAPI.Repository
///
public class WebhookRepository : IWebhookRepository, IInitializer
{
- private readonly IDbConnection _db;
+ private readonly Func _connectionFactory;
- public WebhookRepository(IDbConnection dbService)
+ public WebhookRepository(Func connectionFactory)
{
- _db = dbService;
+ _connectionFactory = connectionFactory;
+ }
+
+ private IDbConnection CreateConnection()
+ {
+ var conn = _connectionFactory();
+ conn.Open();
+ return conn;
}
///
@@ -22,14 +30,27 @@ public WebhookRepository(IDbConnection dbService)
///
public async Task InitializeAsync()
{
- // Define the table structure with a unique constraint on the Url to prevent duplicates
+ using var db = CreateConnection();
+
const string sql = @"
CREATE TABLE IF NOT EXISTS WebhookUrls (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
- Url TEXT NOT NULL UNIQUE
+ Url TEXT NOT NULL UNIQUE,
+ Type INTEGER NOT NULL DEFAULT 0
);";
- await _db.ExecuteAsync(sql);
+ await db.ExecuteAsync(sql);
+ try
+ {
+ const string alterTableSql = "ALTER TABLE WebhookUrls ADD COLUMN Type INTEGER NOT NULL DEFAULT 0;";
+ await db.ExecuteAsync(alterTableSql);
+ }
+ catch (SqliteException ex) when (ex.SqliteErrorCode == 1) { }
+ catch (Exception)
+ {
+ //re-throw any critical exceptions
+ throw;
+ }
}
///
@@ -38,20 +59,35 @@ Url TEXT NOT NULL UNIQUE
/// A collection of WebhookUrl objects.
public async Task> GetAllUrlsAsync()
{
+ using var db = CreateConnection();
const string sql = "SELECT Id, Url FROM WebhookUrls ORDER BY Id;";
- // Dapper maps the columns to the WebhookUrl model properties
- return await _db.QueryAsync(sql);
+ return await db.QueryAsync(sql);
+ }
+
+ public async Task> GetUrlsOfTypeAsync(IWebhookService.WebhookType type)
+ {
+ using var db = CreateConnection();
+ const string sql = "SELECT Id, Url, Type FROM WebhookUrls WHERE Type = @Type;";
+ var parameters = new { Type = (int)type };
+
+ return await db.QueryAsync(sql, parameters);
}
///
/// Adds a new URL to the database. Uses INSERT OR IGNORE to handle duplicates gracefully.
///
/// The URL string to add.
- public async Task AddUrlAsync(string url)
+ public async Task AddUrlAsync(string url, IWebhookService.WebhookType type)
{
- // SQLITE specific command to ignore unique constraint errors if URL already exists
- const string sql = "INSERT OR IGNORE INTO WebhookUrls (Url) VALUES (@Url);";
- await _db.ExecuteAsync(sql, new { Url = url });
+ using var db = CreateConnection();
+ const string sql = "INSERT OR IGNORE INTO WebhookUrls (Url, Type) VALUES (@Url, @Type);";
+ var parameters = new
+ {
+ Url = url,
+ Type = (int)type
+ };
+
+ await db.ExecuteAsync(sql, parameters);
}
///
@@ -61,8 +97,9 @@ public async Task AddUrlAsync(string url)
/// The number of rows deleted (0 or 1).
public async Task DeleteUrlAsync(string url)
{
+ using var db = CreateConnection();
const string sql = "DELETE FROM WebhookUrls WHERE Url = @Url;";
- return await _db.ExecuteAsync(sql, new { Url = url });
+ return await db.ExecuteAsync(sql, new { Url = url });
}
}
}
From 6a7314a5d3b8f47ac84d2eb62a8a75de1e8fa663 Mon Sep 17 00:00:00 2001
From: Skyfall1235 <113722636+Skyfall1235@users.noreply.github.com>
Date: Thu, 4 Dec 2025 01:35:18 -0500
Subject: [PATCH 2/6] db changes + event repo regorganisation
---
.../APIServices/Services/DatabaseService.cs | 4 +-
src/RandomAPI/Repository/DBInitialization.cs | 29 --------------
src/RandomAPI/Repository/EventRepository.cs | 39 +++++++++++++------
3 files changed, 29 insertions(+), 43 deletions(-)
delete mode 100644 src/RandomAPI/Repository/DBInitialization.cs
diff --git a/src/RandomAPI/APIServices/Services/DatabaseService.cs b/src/RandomAPI/APIServices/Services/DatabaseService.cs
index 978643e..7f37ce8 100644
--- a/src/RandomAPI/APIServices/Services/DatabaseService.cs
+++ b/src/RandomAPI/APIServices/Services/DatabaseService.cs
@@ -24,7 +24,7 @@ public void Execute(Action action)
///
/// Opens a connection, executes a Dapper query, and returns a collection of results.
///
- /// The type of object to map the result rows to (e.g., Event).
+ /// The Type of object to map the result rows to (e.g., Event).
/// The SQL SELECT statement to execute.
/// Optional parameters object for Dapper.
/// A collection of mapped objects.
@@ -55,7 +55,7 @@ public async Task ExecuteAsync(string sql, object? param = null)
///
/// Opens a connection, executes a command, and returns a single value (e.g., the last inserted ID).
///
- /// The type of the scalar result (e.g., int).
+ /// The Type of the scalar result (e.g., int).
/// The SQL command to execute.
/// Optional parameters object for Dapper.
/// The single scalar result.
diff --git a/src/RandomAPI/Repository/DBInitialization.cs b/src/RandomAPI/Repository/DBInitialization.cs
deleted file mode 100644
index 3d2b41d..0000000
--- a/src/RandomAPI/Repository/DBInitialization.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System.Data;
-using Dapper;
-
-public static class DBInitialization
-{
- public const string CONNECTIONSTRING = "Data Source=PersonalDev.db";
-
- public static async Task EnsureDb(IServiceProvider services)
- {
- // Use an isolated scope for startup to safely create the connection
- using var scope = services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
-
- //to make more tasbles, write the sql and await and chain it :)
- var sql =
- @"
- CREATE TABLE IF NOT EXISTS Events (
- Id INTEGER PRIMARY KEY AUTOINCREMENT,
- Timestamp TEXT NOT NULL,
- Service TEXT NOT NULL,
- Type TEXT NOT NULL,
- DataType TEXT,
- JsonData TEXT NOT NULL,
- EventId TEXT NOT NULL,
- CONSTRAINT UQ_EventId UNIQUE (EventId)
- );";
- await db.ExecuteAsync(sql);
- }
-}
diff --git a/src/RandomAPI/Repository/EventRepository.cs b/src/RandomAPI/Repository/EventRepository.cs
index 0695d12..8ea37d1 100644
--- a/src/RandomAPI/Repository/EventRepository.cs
+++ b/src/RandomAPI/Repository/EventRepository.cs
@@ -7,17 +7,25 @@ namespace RandomAPI.Repository
{
public class EventRepository : IEventRepository, IInitializer
{
- private readonly IDbConnection _db;
+ private readonly Func _connectionFactory;
private readonly ILogger _logger;
- public EventRepository(IDbConnection db, ILogger logger)
+ public EventRepository(Func connectionFactory, ILogger logger)
{
- _db = db;
+ _connectionFactory = connectionFactory;
_logger = logger;
}
+ private IDbConnection CreateConnection()
+ {
+ var conn = _connectionFactory();
+ conn.Open();
+ return conn;
+ }
+
public async Task InitializeAsync()
{
+ using var db = CreateConnection();
var sql =
@"
CREATE TABLE IF NOT EXISTS Events (
@@ -30,12 +38,13 @@ CREATE TABLE IF NOT EXISTS Events (
EventId TEXT NOT NULL,
CONSTRAINT UQ_EventId UNIQUE (EventId)
);";
- await _db.ExecuteAsync(sql);
+ await db.ExecuteAsync(sql);
}
///
public async Task AddEventAsync(Event eventModel)
{
+ using var db = CreateConnection();
const string sql =
@"
INSERT INTO Events (Timestamp, EventId, Service, Type, DataType, JsonData)
@@ -45,7 +54,7 @@ INSERT INTO Events (Timestamp, EventId, Service, Type, DataType, JsonData)
try
{
- var newId = await _db.ExecuteScalarAsync(sql, eventModel);
+ var newId = await db.ExecuteScalarAsync(sql, eventModel);
return newId;
}
catch (SqliteException ex) when (ex.SqliteErrorCode == 19) // Error code 19 is 'CONSTRAINT'
@@ -54,7 +63,7 @@ INSERT INTO Events (Timestamp, EventId, Service, Type, DataType, JsonData)
$"WARNING: Duplicate event detected. EventId: {eventModel.EventId}"
);
const string selectExistingSql = "SELECT Id FROM Events WHERE EventId = @EventId";
- var existingId = await _db.ExecuteScalarAsync(
+ var existingId = await db.ExecuteScalarAsync(
selectExistingSql,
new { eventModel.EventId }
);
@@ -70,19 +79,21 @@ INSERT INTO Events (Timestamp, EventId, Service, Type, DataType, JsonData)
///
public async Task> GetAllEventsAsync()
{
+ using var db = CreateConnection();
// Retrieves all records, ordered by newest first.
const string sql = "SELECT * FROM Events ORDER BY Timestamp DESC";
- var events = await _db.QueryAsync(sql);
+ var events = await db.QueryAsync(sql);
return events;
}
///
public async Task> GetRangeOfRecentEventsAsync(int count)
{
+ using var db = CreateConnection();
const string sql = "SELECT * FROM Events ORDER BY Timestamp DESC LIMIT @Count";
- var events = await _db.QueryAsync(sql, new { Count = count });
+ var events = await db.QueryAsync(sql, new { Count = count });
return events;
}
@@ -92,18 +103,20 @@ public async Task> GetEventsByIdsAsync(IEnumerable ids)
if (ids == null || !ids.Any())
return Enumerable.Empty();
+ using var db = CreateConnection();
const string sql = "SELECT * FROM Events WHERE Id IN @Ids ORDER BY Timestamp DESC";
- var events = await _db.QueryAsync(sql, new { Ids = ids });
+ var events = await db.QueryAsync(sql, new { Ids = ids });
return events;
}
///
public async Task RemoveEventAsync(int id)
{
+ using var db = CreateConnection();
const string sql = "DELETE FROM Events WHERE Id = @Id";
- var rowsAffected = await _db.ExecuteAsync(sql, new { Id = id });
+ var rowsAffected = await db.ExecuteAsync(sql, new { Id = id });
return rowsAffected;
}
@@ -112,11 +125,13 @@ public async Task RemoveEventsByIdsAsync(IEnumerable ids)
{
if (ids == null || !ids.Any())
return 0;
-
+ using var db = CreateConnection();
const string sql = "DELETE FROM Events WHERE Id IN @Ids";
- var rowsAffected = await _db.ExecuteAsync(sql, new { Ids = ids });
+ var rowsAffected = await db.ExecuteAsync(sql, new { Ids = ids });
return rowsAffected;
}
}
}
+
+
From 3b5b72ef16ca008c8f11f70285d2fbe121b38e8e Mon Sep 17 00:00:00 2001
From: Wyatt Murray <113722636+Skyfall1235@users.noreply.github.com>
Date: Thu, 4 Dec 2025 01:44:02 -0500
Subject: [PATCH 3/6] Upgrade CodeQL actions to version 4
Updated CodeQL action versions to v4 for initialization and analysis.
---
.github/workflows/CodeQL_PR_Analysis.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/CodeQL_PR_Analysis.yml b/.github/workflows/CodeQL_PR_Analysis.yml
index 528deeb..df1c07d 100644
--- a/.github/workflows/CodeQL_PR_Analysis.yml
+++ b/.github/workflows/CodeQL_PR_Analysis.yml
@@ -31,7 +31,7 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3
+ uses: github/codeql-action/init@v4
with:
languages: csharp
@@ -39,4 +39,4 @@ jobs:
run: dotnet build src/RandomAPI/RandomAPI.csproj
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v4
From 16641ff72fbb90eaa879b6b90e4c5b0eb321c06f Mon Sep 17 00:00:00 2001
From: Wyatt Murray <113722636+Skyfall1235@users.noreply.github.com>
Date: Thu, 4 Dec 2025 01:45:46 -0500
Subject: [PATCH 4/6] Potential fix for code scanning alert no. 6: Log entries
created from user input
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
src/RandomAPI/APIServices/Services/BaseWebhookService.cs | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/RandomAPI/APIServices/Services/BaseWebhookService.cs b/src/RandomAPI/APIServices/Services/BaseWebhookService.cs
index 3c9fe52..3dd46a6 100644
--- a/src/RandomAPI/APIServices/Services/BaseWebhookService.cs
+++ b/src/RandomAPI/APIServices/Services/BaseWebhookService.cs
@@ -29,10 +29,11 @@ public async Task HandleRegisterActionAsync([FromBody] string 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}", url);
+ _logger.LogInformation("Registered new webhook listener: {Url}", safeUrlForLog);
return new OkObjectResult(new { Message = $"Listener added successfully: {url}" });
}
@@ -41,6 +42,7 @@ public async Task HandleUnregisterActionAsync([FromBody] string u
{
if (string.IsNullOrWhiteSpace(url))
{
+ var safeUrlForLog = url.Replace("\r", "").Replace("\n", "");
return new BadRequestObjectResult("URL cannot be empty.");
}
url = url.Trim();
@@ -52,7 +54,7 @@ public async Task HandleUnregisterActionAsync([FromBody] string u
return new NotFoundObjectResult(new { Message = $"URL not found: {url}" });
}
- _logger.LogInformation("Unregistered webhook listener: {Url}", url);
+ _logger.LogInformation("Unregistered webhook listener: {Url}", safeUrlForLog);
return new OkObjectResult(new { Message = $"Listener removed: {url}" });
}
From 43a1670cd621f0727cc4bdee8477f4bd45d5fc3e Mon Sep 17 00:00:00 2001
From: Wyatt Murray <113722636+Skyfall1235@users.noreply.github.com>
Date: Thu, 4 Dec 2025 01:45:57 -0500
Subject: [PATCH 5/6] Potential fix for code scanning alert no. 8: Log entries
created from user input
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
src/RandomAPI/Controllers/WebhookController.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/RandomAPI/Controllers/WebhookController.cs b/src/RandomAPI/Controllers/WebhookController.cs
index 4744884..229e6fb 100644
--- a/src/RandomAPI/Controllers/WebhookController.cs
+++ b/src/RandomAPI/Controllers/WebhookController.cs
@@ -86,7 +86,7 @@ public async Task BroadcastDiscordTest([FromBody] DiscordWebhookP
if (!listeners.Any())
return BadRequest("No listeners registered to broadcast to.");
- _logger.LogInformation("Broadcasting Discord payload: {Message}", payload.content);
+ _logger.LogInformation("Broadcasting Discord payload: {Message}", payload.content?.Replace("\r", "").Replace("\n", ""));
await _webhookService.BroadcastAsync(payload);
From ad12b5caabe938157298fe8b7a1a88853ca0111e Mon Sep 17 00:00:00 2001
From: Skyfall1235 <113722636+Skyfall1235@users.noreply.github.com>
Date: Thu, 4 Dec 2025 01:51:00 -0500
Subject: [PATCH 6/6] Update BaseWebhookService.cs
---
src/RandomAPI/APIServices/Services/BaseWebhookService.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/RandomAPI/APIServices/Services/BaseWebhookService.cs b/src/RandomAPI/APIServices/Services/BaseWebhookService.cs
index 3dd46a6..1884ff1 100644
--- a/src/RandomAPI/APIServices/Services/BaseWebhookService.cs
+++ b/src/RandomAPI/APIServices/Services/BaseWebhookService.cs
@@ -40,9 +40,10 @@ public async Task HandleRegisterActionAsync([FromBody] string url
public async Task HandleUnregisterActionAsync([FromBody] string url)
{
+ string safeUrlForLog = url;
if (string.IsNullOrWhiteSpace(url))
{
- var safeUrlForLog = url.Replace("\r", "").Replace("\n", "");
+ safeUrlForLog = url.Replace("\r", "").Replace("\n", "");
return new BadRequestObjectResult("URL cannot be empty.");
}
url = url.Trim();