using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Sozsoft.Notifications.Entities; using Sozsoft.Notifications.Enums; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.Identity; namespace Sozsoft.Notifications.Domain; public interface INotificationManager : ITransientDependency { /// /// Bir bildirim tipinin tum kurallarini isletir ve her alici icin kanal basina bir bildirim /// kaydi uretir. Kayitlarin kanaldan cikisi NotificationWorker'a aittir. /// Task SendNotificationAsync(Guid NotificationTypeId, string NotificationMessage); } public class NotificationManager : INotificationManager { private readonly IReadOnlyRepository repository; private readonly IReadOnlyRepository repositoryUser; private readonly IReadOnlyRepository repositoryRole; private readonly IRepository repositoryNotification; private readonly INotificationIdentifierProvider notificationIdentifierProvider; private readonly IOrganizationUnitRepository repositoryOrganizationUnit; private readonly INotificationRealtimeNotifier realtimeNotifier; public NotificationManager( IReadOnlyRepository repository, IReadOnlyRepository repositoryUser, IReadOnlyRepository repositoryRole, IRepository repositoryNotification, INotificationIdentifierProvider notificationIdentifierProvider, IOrganizationUnitRepository repositoryOrganizationUnit, INotificationRealtimeNotifier realtimeNotifier ) { this.repository = repository; this.repositoryUser = repositoryUser; this.repositoryRole = repositoryRole; this.repositoryNotification = repositoryNotification; this.notificationIdentifierProvider = notificationIdentifierProvider; this.repositoryOrganizationUnit = repositoryOrganizationUnit; this.realtimeNotifier = realtimeNotifier; } public async Task SendNotificationAsync(Guid NotificationTypeId, string NotificationMessage) { var rules = await repository.GetListAsync(a => a.NotificationTypeId == NotificationTypeId); // Kullanici tercihleri (profil ekranindan yazilan IsCustomized kayitlari) alici kumesini // belirlemez; yalnizca sahibinin o kanaldaki aboneligini kapatir. Tercihi kural gibi // isletmek, bir kullanicinin kendi tercihini yazmasiyla ayni tur/kanaldaki herkesin // bildirimini kesmesi anlamina gelirdi. var optedOut = rules .Where(rule => rule.IsCustomized && !rule.IsActive && rule.RecipientType == NotificationRecipientTypes.User && rule.RecipientId != null) .Select(rule => (rule.Channel, UserName: rule.RecipientId!)) .ToHashSet(); // Ayni kullaniciyi birden fazla kural hedefleyebilir (All + Role gibi); kanal basina tek // kayit uretilir. var delivered = new HashSet<(string Channel, string Identifier)>(); var notifications = new List(); var queryUsers = await repositoryUser.GetQueryableAsync(); foreach (var notificationRule in rules.Where(rule => rule.IsActive && !rule.IsCustomized)) { // "Custom" alicida RecipientId adresin kendisidir (telefon, e-posta, chat id); // sistemde bir kullaniciya karsilik gelmeyebilir, bu yuzden kullanici aramasi yapilmaz. if (notificationRule.RecipientType == NotificationRecipientTypes.Custom) { if (notificationRule.RecipientId.IsNullOrWhiteSpace() || !delivered.Add((notificationRule.Channel, notificationRule.RecipientId!))) { continue; } notifications.Add(BuildNotification(notificationRule, notificationRule.RecipientId!, userId: null, NotificationMessage)); continue; } var userIds = await ResolveRecipientUserIdsAsync(notificationRule, queryUsers); if (userIds.Count == 0) { continue; } var users = await queryUsers .Where(user => userIds.Contains(user.Id)) .Select(user => new { user.Id, user.UserName }) .ToListAsync(); foreach (var user in users) { // Zorunlu bildirimde (IsFixed) kullanici tercihi dikkate alinmaz. if (!notificationRule.IsFixed && optedOut.Contains((notificationRule.Channel, user.UserName))) { continue; } var identifier = await notificationIdentifierProvider.GetIdentifier(user.Id, notificationRule.Channel); if (identifier.IsNullOrWhiteSpace() || !delivered.Add((notificationRule.Channel, identifier!))) { continue; } notifications.Add(BuildNotification(notificationRule, identifier!, user.Id, NotificationMessage)); } } if (notifications.Count == 0) { return; } await repositoryNotification.InsertManyAsync(notifications, autoSave: true); // Duyuru kayit yazildiktan sonra yapilir; istemci duyuruyu alip listesini okudugunda // kaydi mutlaka gormeli. Duyuru gitmezse yedek yoklama ayni kaydi getirir. await realtimeNotifier.NotifyAsync( [.. notifications .Where(notification => notification.UserId.HasValue) .Select(notification => new NotificationDelivery(notification.UserId!.Value, notification.NotificationChannel))]); } /// Kuralin hedefledigi kullanicilar; Custom alici bu yoldan gecmez. private async Task> ResolveRecipientUserIdsAsync(NotificationRule notificationRule, IQueryable queryUsers) { List userIds = []; if (notificationRule.RecipientType == NotificationRecipientTypes.All) { userIds.AddRange(await queryUsers .Where(user => user.IsActive) .Select(user => user.Id) .ToListAsync()); } else if (notificationRule.RecipientType == NotificationRecipientTypes.User) { var user = await queryUsers.FirstOrDefaultAsync(user => user.UserName == notificationRule.RecipientId); if (user != null) { userIds.Add(user.Id); } } else if (notificationRule.RecipientType == NotificationRecipientTypes.Role) { var role = await repositoryRole.FirstOrDefaultAsync(role => role.Name == notificationRule.RecipientId); if (role != null) { userIds.AddRange(await queryUsers .Where(user => user.Roles.Any(userRole => userRole.RoleId == role.Id) && user.IsActive) .Select(user => user.Id) .ToListAsync()); } } else if (notificationRule.RecipientType == NotificationRecipientTypes.OrganizationUnit) { if (notificationRule.RecipientId.IsNullOrWhiteSpace()) { return userIds; } var units = await repositoryOrganizationUnit.GetAllChildrenWithParentCodeAsync(notificationRule.RecipientId, null); foreach (var unit in units) { userIds.AddRange(await repositoryOrganizationUnit.GetMemberIdsAsync(unit.Id)); } } return [.. userIds.Distinct()]; } private static Notification BuildNotification(NotificationRule notificationRule, string identifier, Guid? userId, string message) => new() { NotificationTypeId = notificationRule.NotificationTypeId, NotificationRuleId = notificationRule.Id, NotificationChannel = notificationRule.Channel, Identifier = identifier, UserId = userId, Message = message, IsSent = IsDeliveredOnCreate(notificationRule.Channel), IsRead = false }; /// /// Kanalin gonderimi kaydin kendisi mi. UiActivity'nin ayri bir gonderim adimi yoktur — /// kayit dogrudan kullanicinin bildirim akisina yazilir, bu yuzden olusurken iletilmis /// sayilir. Diger kanallar NotificationWorker ya da tarayici gosterimi ile isaretlenir; /// isaretlenmeden okunamazlar. /// private static bool IsDeliveredOnCreate(string channel) => channel == NotificationChannels.UiActivity; }