using Microsoft.Extensions.Logging; using StellaOps.Notify.Models; using System.Net.Http.Json; using System.Text.Json; namespace StellaOps.Notifier.Worker.Channels; /// /// Channel adapter for Slack webhook delivery. /// public sealed class SlackChannelAdapter : INotifyChannelAdapter { private readonly HttpClient _httpClient; private readonly ILogger _logger; public SlackChannelAdapter(HttpClient httpClient, ILogger logger) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public NotifyChannelType ChannelType => NotifyChannelType.Slack; public async Task SendAsync( NotifyChannel channel, NotifyDeliveryRendered rendered, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(channel); ArgumentNullException.ThrowIfNull(rendered); var endpoint = channel.Config?.Endpoint; if (string.IsNullOrWhiteSpace(endpoint)) { return ChannelDispatchResult.Fail("Slack webhook URL not configured", shouldRetry: false); } if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri)) { return ChannelDispatchResult.Fail($"Invalid Slack webhook URL: {endpoint}", shouldRetry: false); } // Build Slack message payload var slackPayload = new { channel = channel.Config?.Target, text = rendered.Title, blocks = new object[] { new { type = "section", text = new { type = "mrkdwn", text = rendered.Body } } } }; try { using var request = new HttpRequestMessage(HttpMethod.Post, uri); request.Content = JsonContent.Create(slackPayload, options: new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); var statusCode = (int)response.StatusCode; if (response.IsSuccessStatusCode) { _logger.LogInformation( "Slack delivery to channel {Target} succeeded.", channel.Config?.Target ?? "(default)"); return ChannelDispatchResult.Ok(statusCode); } var shouldRetry = statusCode >= 500 || statusCode == 429; _logger.LogWarning( "Slack delivery failed with status {StatusCode}. Retry: {ShouldRetry}.", statusCode, shouldRetry); return ChannelDispatchResult.Fail( $"HTTP {statusCode}", shouldRetry: shouldRetry, httpStatusCode: statusCode); } catch (HttpRequestException ex) { _logger.LogError(ex, "Slack delivery failed with network error."); return ChannelDispatchResult.Fail(ex.Message, shouldRetry: true); } catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (TaskCanceledException ex) { _logger.LogWarning(ex, "Slack delivery timed out."); return ChannelDispatchResult.Fail("Request timeout", shouldRetry: true); } } }