Skip to content

Commit 920645d

Browse files
committed
2 parents 56cb131 + f6b9f96 commit 920645d

14 files changed

Lines changed: 317 additions & 171 deletions

File tree

.github/workflows/CodeQL_PR_Analysis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ jobs:
3131
dotnet-version: '8.0.x'
3232

3333
- name: Initialize CodeQL
34-
uses: github/codeql-action/init@v3
34+
uses: github/codeql-action/init@v4
3535
with:
3636
languages: csharp
3737

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

4141
- name: Perform CodeQL Analysis
42-
uses: github/codeql-action/analyze@v3
42+
uses: github/codeql-action/analyze@v4

src/RandomAPI/APIServices/ServiceInterfaces/IWebhookService.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using RandomAPI.Models;
3+
using RandomAPI.Services.Webhooks;
4+
15
public interface IWebhookService
26
{
37
/// <summary>
@@ -9,16 +13,36 @@ public interface IWebhookService
913
/// Registers a new webhook listener URL.
1014
/// </summary>
1115
/// <returns>True if added, false if it already existed.</returns>
12-
bool AddListener(string url);
16+
Task AddListenerAsync(string url, WebhookType type = default);
1317

1418
/// <summary>
1519
/// Removes a webhook listener URL.
1620
/// </summary>
1721
/// <returns>True if removed, false if not found.</returns>
18-
bool RemoveListener(string url);
22+
Task<bool> RemoveListenerAsync(string url);
1923

2024
/// <summary>
2125
/// Returns a snapshot of all registered listener URLs.
2226
/// </summary>
23-
IEnumerable<string> GetListeners();
27+
Task<IEnumerable<string>> GetListenersAsync();
28+
29+
/// <summary>
30+
/// returns a snapshot of all registered listenrs of a given type
31+
/// </summary>
32+
/// <param name="type"> the type of url</param>
33+
34+
Task<IEnumerable<string>> GetListenersAsync(WebhookType type = WebhookType.Default);
35+
36+
// Controller Logic Methods (Implemented in the derived class)
37+
public Task<IActionResult> HandleGetListenersActionAsync();
38+
public Task<IActionResult> HandleGetListenersOfTypeAsync(WebhookType type);
39+
public Task<IActionResult> HandleRegisterActionAsync(string url, WebhookType type = default);
40+
public Task<IActionResult> HandleUnregisterActionAsync(string url);
41+
public Task<IActionResult> HandleBroadcastActionAsync(IWebHookPayload payload);
42+
43+
public enum WebhookType
44+
{
45+
Default = 0,
46+
Discord = 1,
47+
}
2448
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using RandomAPI.Models;
3+
using RandomAPI.Repository;
4+
using static IWebhookService;
5+
6+
namespace RandomAPI.Services.Webhooks
7+
{
8+
public class WebhookActionService : BaseWebhookService, IWebhookService
9+
{
10+
11+
public WebhookActionService(IWebhookRepository repo, ILogger<IWebhookService> logger)
12+
: base(repo, logger) { }
13+
14+
public async Task<IActionResult> HandleGetListenersActionAsync()
15+
{
16+
var urls = await base.GetListenersAsync();
17+
return new OkObjectResult(urls);
18+
}
19+
20+
public async Task<IActionResult> HandleGetListenersOfTypeAsync(WebhookType type)
21+
{
22+
var urls = await base.GetListenersAsync(type);
23+
return new OkObjectResult(urls);
24+
}
25+
26+
public async Task<IActionResult> HandleRegisterActionAsync([FromBody] string url, IWebhookService.WebhookType type = default)
27+
{
28+
if (string.IsNullOrWhiteSpace(url))
29+
return new BadRequestObjectResult("URL cannot be empty.");
30+
//neede both on regisdter and deregister
31+
url = url.Trim();
32+
var safeUrlForLog = url.Replace("\r", "").Replace("\n", "");
33+
34+
await base.AddListenerAsync(url, type);
35+
36+
_logger.LogInformation("Registered new webhook listener: {Url}", safeUrlForLog);
37+
38+
return new OkObjectResult(new { Message = $"Listener added successfully: {url}" });
39+
}
40+
41+
public async Task<IActionResult> HandleUnregisterActionAsync([FromBody] string url)
42+
{
43+
string safeUrlForLog = url;
44+
if (string.IsNullOrWhiteSpace(url))
45+
{
46+
safeUrlForLog = url.Replace("\r", "").Replace("\n", "");
47+
return new BadRequestObjectResult("URL cannot be empty.");
48+
}
49+
url = url.Trim();
50+
51+
var removed = await base.RemoveListenerAsync(url);
52+
53+
if (!removed)
54+
{
55+
return new NotFoundObjectResult(new { Message = $"URL not found: {url}" });
56+
}
57+
58+
_logger.LogInformation("Unregistered webhook listener: {Url}", safeUrlForLog);
59+
return new OkObjectResult(new { Message = $"Listener removed: {url}" });
60+
}
61+
62+
public async Task<IActionResult> HandleBroadcastActionAsync([FromBody] IWebHookPayload payload)
63+
{
64+
var listeners = await base.GetListenersAsync();
65+
66+
if (!listeners.Any())
67+
return new BadRequestObjectResult("No listeners registered to broadcast to.");
68+
69+
switch (payload)
70+
{
71+
case WebhookPayload p:
72+
p.Timestamp = DateTime.UtcNow;
73+
74+
break;
75+
76+
case DiscordWebhookPayload p:
77+
break;
78+
79+
default:
80+
_logger.LogWarning("Received unsupported payload type: {Type}", payload.GetType().Name);
81+
return new BadRequestObjectResult(new { Message = "Unsupported webhook payload type." });
82+
}
83+
84+
_logger.LogInformation("Broadcasting test payload: {Message}", payload.content);
85+
await base.BroadcastAsync(payload);
86+
return new OkObjectResult(new
87+
{
88+
Message = $"Broadcast sent for message: '{payload.content}'. Check logs for delivery status."
89+
});
90+
}
91+
}
92+
93+
94+
public class BaseWebhookService
95+
{
96+
protected readonly IWebhookRepository _repo;
97+
protected readonly HttpClient _client = new();
98+
protected readonly ILogger<IWebhookService> _logger;
99+
100+
public BaseWebhookService(IWebhookRepository repo, ILogger<IWebhookService> logger)
101+
{
102+
_repo = repo;
103+
_logger = logger;
104+
}
105+
106+
public async Task<IEnumerable<string>> GetListenersAsync()
107+
{
108+
var urls = await _repo.GetAllUrlsAsync();
109+
return urls.Select(u => u.Url);
110+
}
111+
112+
public async Task<IEnumerable<string>> GetListenersAsync(WebhookType type = WebhookType.Default)
113+
{
114+
var urls = await _repo.GetUrlsOfTypeAsync(type);
115+
return urls.Select(u => u.Url);
116+
}
117+
118+
public async Task AddListenerAsync(string url, WebhookType type = default)
119+
{
120+
await _repo.AddUrlAsync(url, type);
121+
}
122+
123+
public async Task<bool> RemoveListenerAsync(string url)
124+
{
125+
var result = await _repo.DeleteUrlAsync(url);
126+
return result > 0;
127+
}
128+
129+
//basic broadcast for all
130+
public async Task BroadcastAsync<T>(T payload) where T : class
131+
{
132+
IEnumerable<string> urls = await GetListenersAsync();
133+
await BroadcastAsync(payload, urls);
134+
}
135+
//derived for the payloads
136+
public async Task BroadcastAsync<T>(T payload, IEnumerable<string> urls) where T : class
137+
{
138+
var tasks = urls.Select(async url =>
139+
{
140+
try
141+
{
142+
await _client.PostAsJsonAsync(url, payload);
143+
}
144+
catch (Exception ex)
145+
{
146+
_logger.LogWarning(ex, "Webhook POST failed for URL: {url}", url);
147+
}
148+
});
149+
await Task.WhenAll(tasks);
150+
}
151+
}
152+
}

src/RandomAPI/APIServices/Services/DatabaseService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public void Execute(Action<SqliteConnection> action)
2424
/// <summary>
2525
/// Opens a connection, executes a Dapper query, and returns a collection of results.
2626
/// </summary>
27-
/// <typeparam name="T">The type of object to map the result rows to (e.g., Event).</typeparam>
27+
/// <typeparam name="T">The Type of object to map the result rows to (e.g., Event).</typeparam>
2828
/// <param name="sql">The SQL SELECT statement to execute.</param>
2929
/// <param name="param">Optional parameters object for Dapper.</param>
3030
/// <returns>A collection of mapped objects.</returns>
@@ -55,7 +55,7 @@ public async Task<int> ExecuteAsync(string sql, object? param = null)
5555
/// <summary>
5656
/// Opens a connection, executes a command, and returns a single value (e.g., the last inserted ID).
5757
/// </summary>
58-
/// <typeparam name="T">The type of the scalar result (e.g., int).</typeparam>
58+
/// <typeparam name="T">The Type of the scalar result (e.g., int).</typeparam>
5959
/// <param name="sql">The SQL command to execute.</param>
6060
/// <param name="param">Optional parameters object for Dapper.</param>
6161
/// <returns>The single scalar result.</returns>

src/RandomAPI/APIServices/Services/WebhookPayload.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using RandomAPI.Models;
2+
using RandomAPI.Services.Webhooks;
3+
14
namespace RandomAPI.Services.Webhooks
25
{
36
public class WebhookPayload : ICustomWebhookPayload
@@ -10,4 +13,4 @@ public class DiscordWebhookPayload : IWebHookPayload
1013
{
1114
public string content { get; set; } = "";
1215
}
13-
}
16+
}

src/RandomAPI/APIServices/Services/WebhookService.cs

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)