feat(push): add service-owned firebase channels and delivery
Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation. Ref: IT-1074
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
namespace HrynCo.NotificationService.Worker.Services.Push;
|
||||
|
||||
using FirebaseAdmin.Messaging;
|
||||
using FirebaseAdmin;
|
||||
using Google.Apis.Auth.OAuth2;
|
||||
using HrynCo.NotificationService.Contracts.Messages;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Push;
|
||||
using HrynCo.NotificationService.DAL.Abstract.PushChannels;
|
||||
|
||||
public sealed class FirebasePushClient : IFirebasePushClient
|
||||
{
|
||||
public async Task<PushDeliveryOutcome> SendAsync(Guid messageId, SendPushMessageData data,
|
||||
FirebaseServiceAccountCredentials credentials, CancellationToken cancellationToken)
|
||||
{
|
||||
var remaining = data.ExpiresAt - DateTimeOffset.UtcNow;
|
||||
if (remaining <= TimeSpan.Zero) return new PushDeliveryOutcome("Push expired.");
|
||||
return await SendMessageAsync(new Message
|
||||
{
|
||||
Token = data.Token,
|
||||
Android = new AndroidConfig { Priority = Priority.High, TimeToLive = remaining },
|
||||
Data = new Dictionary<string, string>
|
||||
{
|
||||
["version"] = "1", ["messageId"] = messageId.ToString("D"),
|
||||
["bindingId"] = data.BindingId.ToString("D"), ["title"] = data.Title,
|
||||
["body"] = data.Body, ["route"] = data.Route,
|
||||
["expiresAt"] = data.ExpiresAt.ToUnixTimeSeconds().ToString(System.Globalization.CultureInfo.InvariantCulture)
|
||||
}
|
||||
}, credentials, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<PushDeliveryOutcome> SendTestAsync(string token, FirebaseServiceAccountCredentials credentials,
|
||||
CancellationToken cancellationToken) => SendMessageAsync(new Message
|
||||
{
|
||||
Token = token,
|
||||
Notification = new Notification
|
||||
{
|
||||
Title = "Notification Service test",
|
||||
Body = "This is a test notification from the channel administration page."
|
||||
},
|
||||
Android = new AndroidConfig { Priority = Priority.High, TimeToLive = TimeSpan.FromMinutes(5) }
|
||||
}, credentials, cancellationToken);
|
||||
|
||||
public async Task<PushDeliveryOutcome> CheckAuthorizationAsync(FirebaseServiceAccountCredentials credentials,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(TimeSpan.FromSeconds(30));
|
||||
try
|
||||
{
|
||||
var credential = CreateCredential(credentials).CreateScoped("https://www.googleapis.com/auth/firebase.messaging");
|
||||
await credential.UnderlyingCredential.GetAccessTokenForRequestAsync(cancellationToken: timeout.Token);
|
||||
return new PushDeliveryOutcome(null);
|
||||
}
|
||||
catch (Google.Apis.Auth.OAuth2.Responses.TokenResponseException)
|
||||
{
|
||||
return new PushDeliveryOutcome("Firebase OAuth authorization failed.");
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return new PushDeliveryOutcome("Firebase authorization check timed out.");
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return new PushDeliveryOutcome("Could not reach Firebase authorization service.");
|
||||
}
|
||||
}
|
||||
|
||||
private static GoogleCredential CreateCredential(FirebaseServiceAccountCredentials credentials) =>
|
||||
GoogleCredential.FromServiceAccountCredential(new ServiceAccountCredential(
|
||||
new ServiceAccountCredential.Initializer(credentials.ClientEmail)
|
||||
{ ProjectId = credentials.ProjectId }.FromPrivateKey(credentials.PrivateKey)));
|
||||
|
||||
private static async Task<PushDeliveryOutcome> SendMessageAsync(Message message,
|
||||
FirebaseServiceAccountCredentials credentials, CancellationToken cancellationToken)
|
||||
{
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(TimeSpan.FromSeconds(30));
|
||||
FirebaseApp? app = null;
|
||||
try
|
||||
{
|
||||
app = FirebaseApp.Create(new AppOptions
|
||||
{
|
||||
ProjectId = credentials.ProjectId,
|
||||
Credential = CreateCredential(credentials)
|
||||
}, "push-" + Guid.NewGuid().ToString("N"));
|
||||
await FirebaseMessaging.GetMessaging(app).SendAsync(message, timeout.Token);
|
||||
return new PushDeliveryOutcome(null);
|
||||
}
|
||||
catch (FirebaseMessagingException exception)
|
||||
{
|
||||
var header = exception.HttpResponse?.Headers.RetryAfter;
|
||||
var retryAfter = header?.Delta ?? (header?.Date - DateTimeOffset.UtcNow) ?? TimeSpan.Zero;
|
||||
return Classify(exception.MessagingErrorCode, retryAfter);
|
||||
}
|
||||
catch (Google.Apis.Auth.OAuth2.Responses.TokenResponseException)
|
||||
{
|
||||
return new PushDeliveryOutcome("FCM authorization failed.");
|
||||
}
|
||||
catch (FirebaseException)
|
||||
{
|
||||
return new PushDeliveryOutcome("FCM configuration failed.");
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new PushRetryException(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
throw new PushRetryException(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
app?.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
internal static PushDeliveryOutcome Classify(MessagingErrorCode? code, TimeSpan retryAfter = default) => code switch
|
||||
{
|
||||
MessagingErrorCode.Unregistered => new PushDeliveryOutcome("FCM registration is no longer valid.", true),
|
||||
MessagingErrorCode.Unavailable or MessagingErrorCode.Internal or MessagingErrorCode.QuotaExceeded =>
|
||||
throw new PushRetryException(retryAfter),
|
||||
MessagingErrorCode.SenderIdMismatch => new PushDeliveryOutcome("FCM sender configuration does not match."),
|
||||
MessagingErrorCode.InvalidArgument => new PushDeliveryOutcome("FCM rejected the push payload."),
|
||||
_ => new PushDeliveryOutcome("FCM authorization or configuration failure.")
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user