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;
}