1631786ce2
Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation. Ref: IT-1074
127 lines
5.8 KiB
C#
127 lines
5.8 KiB
C#
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.")
|
|
};
|
|
}
|