mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
Shard notification hub (#4450)
* Allow for binning of comb IDs by date and value
* Introduce notification hub pool
* Replace device type sharding with comb + range sharding
* Fix proxy interface
* Use enumerable services for multiServiceNotificationHub
* Fix push interface usage
* Fix push notification service dependencies
* Fix push notification keys
* Fixup documentation
* Remove deprecated settings
* Fix tests
* PascalCase method names
* Remove unused request model properties
* Remove unused setting
* Improve DateFromComb precision
* Prefer readonly service enumerable
* Pascal case template holes
* Name TryParse methods TryParse
* Apply suggestions from code review
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
* AllClients is a set of clients and must be deduplicated
* Fix registration start time
* Add logging to initialization of a notification hub
* more logging
* Add lower level logging for hub settings
* Log when connection is resolved
* Improve log message
* Log pushes to notification hub
* temporarily elevate log messages for visibility
* Log in multi-service when relaying to another push service
* Revert to more reasonable logging free of user information
* Fixup merge
Deleting user was extracted to a command in #4803, this updates that work to use just the device ids as I did elsewhere in abd67e8ec
* Do not use bouncy castle exception types
* Add required services for logging
---------
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com>
This commit is contained in:
@ -38,13 +38,13 @@ public class DeviceService : IDeviceService
|
||||
public async Task ClearTokenAsync(Device device)
|
||||
{
|
||||
await _deviceRepository.ClearPushTokenAsync(device.Id);
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Device device)
|
||||
{
|
||||
await _deviceRepository.DeleteAsync(device);
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||
}
|
||||
|
||||
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
||||
|
@ -1,61 +1,31 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class MultiServicePushNotificationService : IPushNotificationService
|
||||
{
|
||||
private readonly List<IPushNotificationService> _services = new List<IPushNotificationService>();
|
||||
private readonly IEnumerable<IPushNotificationService> _services;
|
||||
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
||||
|
||||
public MultiServicePushNotificationService(
|
||||
IHttpClientFactory httpFactory,
|
||||
IDeviceRepository deviceRepository,
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
[FromKeyedServices("implementation")] IEnumerable<IPushNotificationService> services,
|
||||
ILogger<MultiServicePushNotificationService> logger,
|
||||
ILogger<RelayPushNotificationService> relayLogger,
|
||||
ILogger<NotificationsApiPushNotificationService> hubLogger)
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
||||
globalSettings.Installation?.Id != null &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
|
||||
{
|
||||
_services.Add(new RelayPushNotificationService(httpFactory, deviceRepository, globalSettings,
|
||||
httpContextAccessor, relayLogger));
|
||||
}
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
||||
{
|
||||
_services.Add(new NotificationsApiPushNotificationService(
|
||||
httpFactory, globalSettings, httpContextAccessor, hubLogger));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var generalHub = globalSettings.NotificationHubs?.FirstOrDefault(h => h.HubType == NotificationHubType.General);
|
||||
if (CoreHelpers.SettingHasValue(generalHub?.ConnectionString))
|
||||
{
|
||||
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
|
||||
globalSettings, httpContextAccessor, hubLogger));
|
||||
}
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
||||
{
|
||||
_services.Add(new AzureQueuePushNotificationService(globalSettings, httpContextAccessor));
|
||||
}
|
||||
}
|
||||
_services = services;
|
||||
|
||||
_logger = logger;
|
||||
_logger.LogInformation("Hub services: {Services}", _services.Count());
|
||||
globalSettings?.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
|
||||
{
|
||||
_logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
|
||||
});
|
||||
}
|
||||
|
||||
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
|
@ -1,300 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Azure.NotificationHubs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
{
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly List<NotificationHubClient> _clients = [];
|
||||
private readonly bool _enableTracing = false;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public NotificationHubPushNotificationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<NotificationsApiPushNotificationService> logger)
|
||||
{
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
|
||||
foreach (var hub in globalSettings.NotificationHubs)
|
||||
{
|
||||
var client = NotificationHubClient.CreateClientFromConnectionString(
|
||||
hub.ConnectionString,
|
||||
hub.HubName,
|
||||
hub.EnableSendTracing);
|
||||
_clients.Add(client);
|
||||
|
||||
_enableTracing = _enableTracing || hub.EnableSendTracing;
|
||||
}
|
||||
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
||||
}
|
||||
|
||||
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
if (cipher.OrganizationId.HasValue)
|
||||
{
|
||||
// We cannot send org pushes since access logic is much more complicated than just the fact that they belong
|
||||
// to the organization. Potentially we could blindly send to just users that have the access all permission
|
||||
// device registration needs to be more granular to handle that appropriately. A more brute force approach could
|
||||
// me to send "full sync" push to all org users, but that has the potential to DDOS the API in bursts.
|
||||
|
||||
// await SendPayloadToOrganizationAsync(cipher.OrganizationId.Value, type, message, true);
|
||||
}
|
||||
else if (cipher.UserId.HasValue)
|
||||
{
|
||||
var message = new SyncCipherPushNotification
|
||||
{
|
||||
Id = cipher.Id,
|
||||
UserId = cipher.UserId,
|
||||
OrganizationId = cipher.OrganizationId,
|
||||
RevisionDate = cipher.RevisionDate,
|
||||
CollectionIds = collectionIds,
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderCreateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderUpdateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderDeleteAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderDelete);
|
||||
}
|
||||
|
||||
private async Task PushFolderAsync(Folder folder, PushType type)
|
||||
{
|
||||
var message = new SyncFolderPushNotification
|
||||
{
|
||||
Id = folder.Id,
|
||||
UserId = folder.UserId,
|
||||
RevisionDate = folder.RevisionDate
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(folder.UserId, type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushSyncCiphersAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncCiphers);
|
||||
}
|
||||
|
||||
public async Task PushSyncVaultAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncVault);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrganizations);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrgKeysAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrgKeys);
|
||||
}
|
||||
|
||||
public async Task PushSyncSettingsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncSettings);
|
||||
}
|
||||
|
||||
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
|
||||
}
|
||||
|
||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
||||
{
|
||||
var message = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendUpdateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendDeleteAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendDelete);
|
||||
}
|
||||
|
||||
private async Task PushSendAsync(Send send, PushType type)
|
||||
{
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
var message = new SyncSendPushNotification
|
||||
{
|
||||
Id = send.Id,
|
||||
UserId = send.UserId.Value,
|
||||
RevisionDate = send.RevisionDate
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(message.UserId, type, message, true);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
||||
}
|
||||
|
||||
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
|
||||
{
|
||||
var message = new AuthRequestPushNotification
|
||||
{
|
||||
Id = authRequest.Id,
|
||||
UserId = authRequest.UserId
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
|
||||
}
|
||||
|
||||
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
|
||||
{
|
||||
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
|
||||
}
|
||||
|
||||
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext)
|
||||
{
|
||||
await SendPayloadToUserAsync(orgId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
|
||||
}
|
||||
|
||||
public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
|
||||
string deviceId = null)
|
||||
{
|
||||
var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier);
|
||||
await SendPayloadAsync(tag, type, payload);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
|
||||
string deviceId = null)
|
||||
{
|
||||
var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier);
|
||||
await SendPayloadAsync(tag, type, payload);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetContextIdentifier(bool excludeCurrentContext)
|
||||
{
|
||||
if (!excludeCurrentContext)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var currentContext = _httpContextAccessor?.HttpContext?.
|
||||
RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
|
||||
return currentContext?.DeviceIdentifier;
|
||||
}
|
||||
|
||||
private string BuildTag(string tag, string identifier)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
tag += $" && !deviceIdentifier:{SanitizeTagInput(identifier)}";
|
||||
}
|
||||
|
||||
return $"({tag})";
|
||||
}
|
||||
|
||||
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
||||
{
|
||||
var tasks = new List<Task<NotificationOutcome>>();
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
var task = client.SendTemplateNotificationAsync(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "type", ((byte)type).ToString() },
|
||||
{ "payload", JsonSerializer.Serialize(payload) }
|
||||
}, tag);
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
if (_enableTracing)
|
||||
{
|
||||
for (var i = 0; i < tasks.Count; i++)
|
||||
{
|
||||
if (_clients[i].EnableTestSend)
|
||||
{
|
||||
var outcome = await tasks[i];
|
||||
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
|
||||
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string SanitizeTagInput(string input)
|
||||
{
|
||||
// Only allow a-z, A-Z, 0-9, and special characters -_:
|
||||
return Regex.Replace(input, "[^a-zA-Z0-9-_:]", string.Empty);
|
||||
}
|
||||
}
|
@ -1,277 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Azure.NotificationHubs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
{
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
||||
private Dictionary<NotificationHubType, NotificationHubClient> _clients = [];
|
||||
|
||||
public NotificationHubPushRegistrationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<NotificationHubPushRegistrationService> logger)
|
||||
{
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
// Is this dirty to do in the ctor?
|
||||
void addHub(NotificationHubType type)
|
||||
{
|
||||
var hubRegistration = globalSettings.NotificationHubs.FirstOrDefault(
|
||||
h => h.HubType == type && h.EnableRegistration);
|
||||
if (hubRegistration != null)
|
||||
{
|
||||
var client = NotificationHubClient.CreateClientFromConnectionString(
|
||||
hubRegistration.ConnectionString,
|
||||
hubRegistration.HubName,
|
||||
hubRegistration.EnableSendTracing);
|
||||
_clients.Add(type, client);
|
||||
}
|
||||
}
|
||||
|
||||
addHub(NotificationHubType.General);
|
||||
addHub(NotificationHubType.iOS);
|
||||
addHub(NotificationHubType.Android);
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||
string identifier, DeviceType type)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pushToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var installation = new Installation
|
||||
{
|
||||
InstallationId = deviceId,
|
||||
PushChannel = pushToken,
|
||||
Templates = new Dictionary<string, InstallationTemplate>()
|
||||
};
|
||||
|
||||
installation.Tags = new List<string>
|
||||
{
|
||||
$"userId:{userId}"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
installation.Tags.Add("deviceIdentifier:" + identifier);
|
||||
}
|
||||
|
||||
string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null;
|
||||
switch (type)
|
||||
{
|
||||
case DeviceType.Android:
|
||||
var featureService = _serviceProvider.GetRequiredService<IFeatureService>();
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.AnhFcmv1Migration))
|
||||
{
|
||||
payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}";
|
||||
messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," +
|
||||
"\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}";
|
||||
installation.Platform = NotificationPlatform.FcmV1;
|
||||
}
|
||||
else
|
||||
{
|
||||
payloadTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}}";
|
||||
messageTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\"}," +
|
||||
"\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}";
|
||||
installation.Platform = NotificationPlatform.Fcm;
|
||||
}
|
||||
break;
|
||||
case DeviceType.iOS:
|
||||
payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," +
|
||||
"\"aps\":{\"content-available\":1}}";
|
||||
messageTemplate = "{\"data\":{\"type\":\"#(type)\"}," +
|
||||
"\"aps\":{\"alert\":\"$(message)\",\"badge\":null,\"content-available\":1}}";
|
||||
badgeMessageTemplate = "{\"data\":{\"type\":\"#(type)\"}," +
|
||||
"\"aps\":{\"alert\":\"$(message)\",\"badge\":\"#(badge)\",\"content-available\":1}}";
|
||||
|
||||
installation.Platform = NotificationPlatform.Apns;
|
||||
break;
|
||||
case DeviceType.AndroidAmazon:
|
||||
payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}";
|
||||
messageTemplate = "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}";
|
||||
|
||||
installation.Platform = NotificationPlatform.Adm;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier);
|
||||
BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier);
|
||||
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
||||
userId, identifier);
|
||||
|
||||
await GetClient(type).CreateOrUpdateInstallationAsync(installation);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody,
|
||||
string userId, string identifier)
|
||||
{
|
||||
if (templateBody == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fullTemplateId = $"template:{templateId}";
|
||||
|
||||
var template = new InstallationTemplate
|
||||
{
|
||||
Body = templateBody,
|
||||
Tags = new List<string>
|
||||
{
|
||||
fullTemplateId,
|
||||
$"{fullTemplateId}_userId:{userId}"
|
||||
}
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
template.Tags.Add($"{fullTemplateId}_deviceIdentifier:{identifier}");
|
||||
}
|
||||
|
||||
installation.Templates.Add(fullTemplateId, template);
|
||||
}
|
||||
|
||||
public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
||||
{
|
||||
try
|
||||
{
|
||||
await GetClient(deviceType).DeleteInstallationAsync(deviceId);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
{
|
||||
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
||||
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
||||
{
|
||||
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
{
|
||||
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Remove,
|
||||
$"organizationId:{organizationId}");
|
||||
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
||||
{
|
||||
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PatchTagsForUserDevicesAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, UpdateOperationType op,
|
||||
string tag)
|
||||
{
|
||||
if (!devices.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var operation = new PartialUpdateOperation
|
||||
{
|
||||
Operation = op,
|
||||
Path = "/tags"
|
||||
};
|
||||
|
||||
if (op == UpdateOperationType.Add)
|
||||
{
|
||||
operation.Value = tag;
|
||||
}
|
||||
else if (op == UpdateOperationType.Remove)
|
||||
{
|
||||
operation.Path += $"/{tag}";
|
||||
}
|
||||
|
||||
foreach (var device in devices)
|
||||
{
|
||||
try
|
||||
{
|
||||
await GetClient(device.Value).PatchInstallationAsync(device.Key, new List<PartialUpdateOperation> { operation });
|
||||
}
|
||||
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationHubClient GetClient(DeviceType deviceType)
|
||||
{
|
||||
var hubType = NotificationHubType.General;
|
||||
switch (deviceType)
|
||||
{
|
||||
case DeviceType.Android:
|
||||
hubType = NotificationHubType.Android;
|
||||
break;
|
||||
case DeviceType.iOS:
|
||||
hubType = NotificationHubType.iOS;
|
||||
break;
|
||||
case DeviceType.ChromeExtension:
|
||||
case DeviceType.FirefoxExtension:
|
||||
case DeviceType.OperaExtension:
|
||||
case DeviceType.EdgeExtension:
|
||||
case DeviceType.VivaldiExtension:
|
||||
case DeviceType.SafariExtension:
|
||||
hubType = NotificationHubType.GeneralBrowserExtension;
|
||||
break;
|
||||
case DeviceType.WindowsDesktop:
|
||||
case DeviceType.MacOsDesktop:
|
||||
case DeviceType.LinuxDesktop:
|
||||
hubType = NotificationHubType.GeneralDesktop;
|
||||
break;
|
||||
case DeviceType.ChromeBrowser:
|
||||
case DeviceType.FirefoxBrowser:
|
||||
case DeviceType.OperaBrowser:
|
||||
case DeviceType.EdgeBrowser:
|
||||
case DeviceType.IEBrowser:
|
||||
case DeviceType.UnknownBrowser:
|
||||
case DeviceType.SafariBrowser:
|
||||
case DeviceType.VivaldiBrowser:
|
||||
hubType = NotificationHubType.GeneralWeb;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_clients.ContainsKey(hubType))
|
||||
{
|
||||
_logger.LogWarning("No hub client for '{0}'. Using general hub instead.", hubType);
|
||||
hubType = NotificationHubType.General;
|
||||
if (!_clients.ContainsKey(hubType))
|
||||
{
|
||||
throw new Exception("No general hub client found.");
|
||||
}
|
||||
}
|
||||
return _clients[hubType];
|
||||
}
|
||||
}
|
@ -38,37 +38,36 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
|
||||
await SendAsync(HttpMethod.Post, "push/register", requestModel);
|
||||
}
|
||||
|
||||
public async Task DeleteRegistrationAsync(string deviceId, DeviceType type)
|
||||
public async Task DeleteRegistrationAsync(string deviceId)
|
||||
{
|
||||
var requestModel = new PushDeviceRequestModel
|
||||
{
|
||||
Id = deviceId,
|
||||
Type = type,
|
||||
};
|
||||
await SendAsync(HttpMethod.Post, "push/delete", requestModel);
|
||||
}
|
||||
|
||||
public async Task AddUserRegistrationOrganizationAsync(
|
||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
IEnumerable<string> deviceIds, string organizationId)
|
||||
{
|
||||
if (!devices.Any())
|
||||
if (!deviceIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
|
||||
}
|
||||
|
||||
public async Task DeleteUserRegistrationOrganizationAsync(
|
||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
IEnumerable<string> deviceIds, string organizationId)
|
||||
{
|
||||
if (!devices.Any())
|
||||
if (!deviceIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user