DiscordWebhook.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. using System.Net.Http;
  2. using System.Net.Http.Json;
  3. using System.Text.Json;
  4. using System.Text.Json.Serialization;
  5. using System.Threading.Tasks;
  6. namespace Content.Server.Discord;
  7. public sealed class DiscordWebhook : IPostInjectInit
  8. {
  9. private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
  10. { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
  11. [Dependency] private readonly ILogManager _log = default!;
  12. private const string BaseUrl = "https://discord.com/api/v10/webhooks";
  13. private readonly HttpClient _http = new();
  14. private ISawmill _sawmill = default!;
  15. private string GetUrl(WebhookIdentifier identifier)
  16. {
  17. return $"{BaseUrl}/{identifier.Id}/{identifier.Token}";
  18. }
  19. /// <summary>
  20. /// Gets the webhook data from the given webhook url.
  21. /// </summary>
  22. /// <param name="url">The url to get the data from.</param>
  23. /// <returns>The webhook data returned from the url.</returns>
  24. public async Task<WebhookData?> GetWebhook(string url)
  25. {
  26. try
  27. {
  28. return await _http.GetFromJsonAsync<WebhookData>(url);
  29. }
  30. catch (Exception e)
  31. {
  32. _sawmill.Error($"Error getting discord webhook data.\n{e}");
  33. return null;
  34. }
  35. }
  36. /// <summary>
  37. /// Gets the webhook data from the given webhook url.
  38. /// </summary>
  39. /// <param name="url">The url to get the data from.</param>
  40. /// <param name="onComplete">The delegate to invoke with the obtained data, if any.</param>
  41. public async void GetWebhook(string url, Action<WebhookData> onComplete)
  42. {
  43. if (await GetWebhook(url) is { } data)
  44. onComplete(data);
  45. }
  46. /// <summary>
  47. /// Tries to get the webhook data from the given webhook url if it is not null or whitespace.
  48. /// </summary>
  49. /// <param name="url">The url to get the data from.</param>
  50. /// <param name="onComplete">The delegate to invoke with the obtained data, if any.</param>
  51. public async void TryGetWebhook(string url, Action<WebhookData> onComplete)
  52. {
  53. if (await GetWebhook(url) is { } data)
  54. onComplete(data);
  55. }
  56. /// <summary>
  57. /// Creates a new webhook message with the given identifier and payload.
  58. /// </summary>
  59. /// <param name="identifier">The identifier for the webhook url.</param>
  60. /// <param name="payload">The payload to create the message from.</param>
  61. /// <returns>The response from Discord's API.</returns>
  62. public async Task<HttpResponseMessage> CreateMessage(WebhookIdentifier identifier, WebhookPayload payload)
  63. {
  64. var url = $"{GetUrl(identifier)}?wait=true";
  65. var response = await _http.PostAsJsonAsync(url, payload, JsonOptions);
  66. LogResponse(response, "Create");
  67. return response;
  68. }
  69. /// <summary>
  70. /// Deletes a webhook message with the given identifier and message id.
  71. /// </summary>
  72. /// <param name="identifier">The identifier for the webhook url.</param>
  73. /// <param name="messageId">The message id to delete.</param>
  74. /// <returns>The response from Discord's API.</returns>
  75. public async Task<HttpResponseMessage> DeleteMessage(WebhookIdentifier identifier, ulong messageId)
  76. {
  77. var url = $"{GetUrl(identifier)}/messages/{messageId}";
  78. var response = await _http.DeleteAsync(url);
  79. LogResponse(response, "Delete");
  80. return response;
  81. }
  82. /// <summary>
  83. /// Creates a new webhook message with the given identifier, message id and payload.
  84. /// </summary>
  85. /// <param name="identifier">The identifier for the webhook url.</param>
  86. /// <param name="messageId">The message id to edit.</param>
  87. /// <param name="payload">The payload used to edit the message.</param>
  88. /// <returns>The response from Discord's API.</returns>
  89. public async Task<HttpResponseMessage> EditMessage(WebhookIdentifier identifier, ulong messageId, WebhookPayload payload)
  90. {
  91. var url = $"{GetUrl(identifier)}/messages/{messageId}";
  92. var response = await _http.PatchAsJsonAsync(url, payload, JsonOptions);
  93. LogResponse(response, "Edit");
  94. return response;
  95. }
  96. void IPostInjectInit.PostInject()
  97. {
  98. _sawmill = _log.GetSawmill("DISCORD");
  99. }
  100. /// <summary>
  101. /// Logs detailed information about the HTTP response received from a Discord webhook request.
  102. /// If the response status code is non-2XX it logs the status code, relevant rate limit headers.
  103. /// </summary>
  104. /// <param name="response">The HTTP response received from the Discord API.</param>
  105. /// <param name="methodName">The name (constant) of the method that initiated the webhook request (e.g., "Create", "Edit", "Delete").</param>
  106. private void LogResponse(HttpResponseMessage response, string methodName)
  107. {
  108. if (!response.IsSuccessStatusCode)
  109. {
  110. _sawmill.Error($"Failed to {methodName} message. Status code: {response.StatusCode}.");
  111. if (response.Headers.TryGetValues("Retry-After", out var retryAfter))
  112. _sawmill.Debug($"Failed webhook response Retry-After: {string.Join(", ", retryAfter)}");
  113. if (response.Headers.TryGetValues("X-RateLimit-Global", out var globalRateLimit))
  114. _sawmill.Debug($"Failed webhook response X-RateLimit-Global: {string.Join(", ", globalRateLimit)}");
  115. if (response.Headers.TryGetValues("X-RateLimit-Scope", out var rateLimitScope))
  116. _sawmill.Debug($"Failed webhook response X-RateLimit-Scope: {string.Join(", ", rateLimitScope)}");
  117. }
  118. }
  119. }