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; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.Identity; using Volo.Abp.MultiTenancy; using Volo.Abp.Timing; 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); /// /// Alici kumesi kurallardan degil ekrandan gelen gonderim: secilen her kullanici icin secilen /// her kanalda bir bildirim kaydi uretir. Kanaldan cikis yine NotificationWorker'a /// aittir; kullanici tercihi dikkate alinmaz, cunku gonderim zaten bilincli bir aksiyondur. /// Task SendManualNotificationAsync(ManualNotificationRequest request); } /// Manuel gonderim istegi; alicilar ve kanallar cagirandan gelir. public class ManualNotificationRequest { public Guid NotificationTypeId { get; set; } /// Metnin geldigi sablon; secilmediyse bos. Uretilen her satira provenans olarak yazilir. public Guid? NotificationTemplateId { get; set; } public IReadOnlyCollection UserIds { get; set; } = []; public IReadOnlyCollection Channels { get; set; } = []; public string Message { get; set; } = string.Empty; /// Mail konu satiri; bos ise konu bildirim tipinin adindan uretilir. public string Subject { get; set; } } /// /// Manuel gonderimin sonucu. Atlananlar kanal kanal raporlanir: bir alicinin o kanalda adresi /// (e-posta, telefon, chat id) yoksa bildirim uretilemez ve bunun cozumu kullanici kaydindadir. /// public class ManualNotificationResult { public int SentCount { get; set; } /// Uretilen ilk kaydin kimligi; ekran gonderimden sonra bu satira konumlanir. public Guid FirstNotificationId { get; set; } public Dictionary SkippedByChannel { get; set; } = []; public int SkippedCount => SkippedByChannel.Values.Sum(); } 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; private readonly ICurrentTenant currentTenant; private readonly IClock clock; public NotificationManager( IReadOnlyRepository repository, IReadOnlyRepository repositoryUser, IReadOnlyRepository repositoryRole, IRepository repositoryNotification, INotificationIdentifierProvider notificationIdentifierProvider, IOrganizationUnitRepository repositoryOrganizationUnit, INotificationRealtimeNotifier realtimeNotifier, ICurrentTenant currentTenant, IClock clock ) { this.repository = repository; this.repositoryUser = repositoryUser; this.repositoryRole = repositoryRole; this.repositoryNotification = repositoryNotification; this.notificationIdentifierProvider = notificationIdentifierProvider; this.repositoryOrganizationUnit = repositoryOrganizationUnit; this.realtimeNotifier = realtimeNotifier; this.currentTenant = currentTenant; this.clock = clock; } 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 SaveAndAnnounceAsync(notifications); } /// /// Alicilari kurallar yerine cagiranin verdigi gonderim. Kullanici tercihi okunmaz: manuel /// gonderim zorunlu bildirim (IsFixed) gibi davranir, aksi halde yonetici bilincli /// yaptigi gonderimin kime gitmedigini hic ogrenemezdi. /// public async Task SendManualNotificationAsync(ManualNotificationRequest request) { var result = new ManualNotificationResult(); var channels = (request.Channels ?? []) .Where(channel => !channel.IsNullOrWhiteSpace()) .Select(channel => channel.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); var userIds = (request.UserIds ?? []).Distinct().ToList(); if (channels.Count == 0 || userIds.Count == 0 || request.Message.IsNullOrWhiteSpace()) { return result; } // Adm_T_Notification.NotificationRuleId NOT NULL'dur. Manuel gonderim kural motorundan // gecmedigi icin tipin capa kuralina baglanir; kural pasiftir, alici kumesini o belirlemez. var anchorRule = await repository.FirstOrDefaultAsync(rule => rule.NotificationTypeId == request.NotificationTypeId && rule.RecipientType == NotificationRecipientTypes.Custom) ?? await repository.FirstOrDefaultAsync(rule => rule.NotificationTypeId == request.NotificationTypeId) ?? throw new BusinessException("Notifications:ManualAnchorRuleNotFound") .WithData("NotificationTypeId", request.NotificationTypeId); // Kullanici sorgusu ABP kiraci filtresinden gecer; istekteki kimlikler degistirilse bile // baska bir kiracinin kullanicisina bildirim uretilemez. var queryUsers = await repositoryUser.GetQueryableAsync(); var users = await queryUsers .Where(user => userIds.Contains(user.Id) && user.IsActive) .Select(user => new ManualRecipient(user.Id, user.UserName, user.Name, user.Surname, user.Email)) .ToListAsync(); var notifications = new List(); foreach (var channel in channels) { foreach (var user in users) { var identifier = await notificationIdentifierProvider.GetIdentifier(user.Id, channel); if (identifier.IsNullOrWhiteSpace()) { result.SkippedByChannel[channel] = result.SkippedByChannel.GetOrDefault(channel) + 1; continue; } notifications.Add(new Notification { NotificationTypeId = request.NotificationTypeId, NotificationTemplateId = request.NotificationTemplateId, NotificationRuleId = anchorRule.Id, NotificationChannel = channel, Identifier = identifier!, UserId = user.Id, Message = ResolveTokens(request.Message, user), Subject = request.Subject, IsSent = IsDeliveredOnCreate(channel), IsRead = false }); } } result.SentCount = notifications.Count; await SaveAndAnnounceAsync(notifications); result.FirstNotificationId = notifications.Count == 0 ? Guid.Empty : notifications[0].Id; return result; } /// Mesajdaki yer tutucular alici bazinda cozulur; her alici kendi metnini alir. private string ResolveTokens(string message, ManualRecipient recipient) { var now = clock.Now; var resolved = message .Replace(NotificationMessageTokens.FullName, $"{recipient.Name} {recipient.Surname}".Trim()) .Replace(NotificationMessageTokens.UserName, recipient.UserName ?? string.Empty) .Replace(NotificationMessageTokens.Email, recipient.Email ?? string.Empty) .Replace(NotificationMessageTokens.Date, now.ToShortDateString()) .Replace(NotificationMessageTokens.Time, now.ToShortTimeString()) .Replace(NotificationMessageTokens.Tenant, currentTenant.Name ?? string.Empty); // Yer tutucular cozulunce metin uzayabilir; kayit kolonu 2048 karakterdir. return resolved.Length > 2048 ? resolved[..2048] : resolved; } /// /// Kayitlari yazar ve acik istemcilere duyurur. Duyuru kayit yazildiktan sonra yapilir; /// istemci duyuruyu alip listesini okudugunda kaydi mutlaka gormeli. Duyuru gitmezse /// istemcideki yedek yoklama ayni kaydi getirir. /// private async Task SaveAndAnnounceAsync(List notifications) { if (notifications.Count == 0) { return; } await repositoryNotification.InsertManyAsync(notifications, autoSave: true); await realtimeNotifier.NotifyAsync( [.. notifications .Where(notification => notification.UserId.HasValue) .Select(notification => new NotificationDelivery(notification.UserId!.Value, notification.NotificationChannel))]); } /// Yer tutucularin cozulmesi icin gereken alici bilgisi. private sealed record ManualRecipient(Guid Id, string UserName, string Name, string Surname, string Email); /// 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; }