using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Content.Server.Discord;
public sealed class DiscordWebhook : IPostInjectInit
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
[Dependency] private readonly ILogManager _log = default!;
private const string BaseUrl = "https://discord.com/api/v10/webhooks";
private readonly HttpClient _http = new();
private ISawmill _sawmill = default!;
private string GetUrl(WebhookIdentifier identifier)
{
return $"{BaseUrl}/{identifier.Id}/{identifier.Token}";
}
///
/// Gets the webhook data from the given webhook url.
///
/// The url to get the data from.
/// The webhook data returned from the url.
public async Task GetWebhook(string url)
{
try
{
return await _http.GetFromJsonAsync(url);
}
catch (Exception e)
{
_sawmill.Error($"Error getting discord webhook data.\n{e}");
return null;
}
}
///
/// Gets the webhook data from the given webhook url.
///
/// The url to get the data from.
/// The delegate to invoke with the obtained data, if any.
public async void GetWebhook(string url, Action onComplete)
{
if (await GetWebhook(url) is { } data)
onComplete(data);
}
///
/// Tries to get the webhook data from the given webhook url if it is not null or whitespace.
///
/// The url to get the data from.
/// The delegate to invoke with the obtained data, if any.
public async void TryGetWebhook(string url, Action onComplete)
{
if (await GetWebhook(url) is { } data)
onComplete(data);
}
///
/// Creates a new webhook message with the given identifier and payload.
///
/// The identifier for the webhook url.
/// The payload to create the message from.
/// The response from Discord's API.
public async Task CreateMessage(WebhookIdentifier identifier, WebhookPayload payload)
{
var url = $"{GetUrl(identifier)}?wait=true";
var response = await _http.PostAsJsonAsync(url, payload, JsonOptions);
LogResponse(response, "Create");
return response;
}
///
/// Deletes a webhook message with the given identifier and message id.
///
/// The identifier for the webhook url.
/// The message id to delete.
/// The response from Discord's API.
public async Task DeleteMessage(WebhookIdentifier identifier, ulong messageId)
{
var url = $"{GetUrl(identifier)}/messages/{messageId}";
var response = await _http.DeleteAsync(url);
LogResponse(response, "Delete");
return response;
}
///
/// Creates a new webhook message with the given identifier, message id and payload.
///
/// The identifier for the webhook url.
/// The message id to edit.
/// The payload used to edit the message.
/// The response from Discord's API.
public async Task EditMessage(WebhookIdentifier identifier, ulong messageId, WebhookPayload payload)
{
var url = $"{GetUrl(identifier)}/messages/{messageId}";
var response = await _http.PatchAsJsonAsync(url, payload, JsonOptions);
LogResponse(response, "Edit");
return response;
}
void IPostInjectInit.PostInject()
{
_sawmill = _log.GetSawmill("DISCORD");
}
///
/// Logs detailed information about the HTTP response received from a Discord webhook request.
/// If the response status code is non-2XX it logs the status code, relevant rate limit headers.
///
/// The HTTP response received from the Discord API.
/// The name (constant) of the method that initiated the webhook request (e.g., "Create", "Edit", "Delete").
private void LogResponse(HttpResponseMessage response, string methodName)
{
if (!response.IsSuccessStatusCode)
{
_sawmill.Error($"Failed to {methodName} message. Status code: {response.StatusCode}.");
if (response.Headers.TryGetValues("Retry-After", out var retryAfter))
_sawmill.Debug($"Failed webhook response Retry-After: {string.Join(", ", retryAfter)}");
if (response.Headers.TryGetValues("X-RateLimit-Global", out var globalRateLimit))
_sawmill.Debug($"Failed webhook response X-RateLimit-Global: {string.Join(", ", globalRateLimit)}");
if (response.Headers.TryGetValues("X-RateLimit-Scope", out var rateLimitScope))
_sawmill.Debug($"Failed webhook response X-RateLimit-Scope: {string.Join(", ", rateLimitScope)}");
}
}
}