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
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..1884ff1
--- /dev/null
+++ b/src/RandomAPI/APIServices/Services/BaseWebhookService.cs
@@ -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 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();
+ 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 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 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/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/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..229e6fb 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?.Replace("\r", "").Replace("\n", ""));
- _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 7a824e9..3d02038 100644
Binary files a/src/RandomAPI/PersonalDev.db and b/src/RandomAPI/PersonalDev.db differ
diff --git a/src/RandomAPI/Program.cs b/src/RandomAPI/Program.cs
index 8fc75b1..7e769de 100644
--- a/src/RandomAPI/Program.cs
+++ b/src/RandomAPI/Program.cs
@@ -12,13 +12,20 @@
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
-builder.Services.AddTransient(_ => 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/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;
}
}
}
+
+
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 });
}
}
}