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 }); } } }