225 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			225 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using DysonNetwork.Shared;
 | 
						|
using DysonNetwork.Shared.Cache;
 | 
						|
using DysonNetwork.Shared.Models;
 | 
						|
using DysonNetwork.Shared.Proto;
 | 
						|
using DysonNetwork.Sphere.Localization;
 | 
						|
using Microsoft.EntityFrameworkCore;
 | 
						|
using Microsoft.Extensions.Localization;
 | 
						|
 | 
						|
namespace DysonNetwork.Sphere.Publisher;
 | 
						|
 | 
						|
public class PublisherSubscriptionService(
 | 
						|
    AppDatabase db,
 | 
						|
    Post.PostService ps,
 | 
						|
    IStringLocalizer<NotificationResource> localizer,
 | 
						|
    ICacheService cache,
 | 
						|
    RingService.RingServiceClient pusher,
 | 
						|
    AccountService.AccountServiceClient accounts
 | 
						|
)
 | 
						|
{
 | 
						|
    /// <summary>
 | 
						|
    /// Checks if a subscription exists between the account and publisher
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="accountId">The account ID</param>
 | 
						|
    /// <param name="publisherId">The publisher ID</param>
 | 
						|
    /// <returns>True if a subscription exists, false otherwise</returns>
 | 
						|
    public async Task<bool> SubscriptionExistsAsync(Guid accountId, Guid publisherId)
 | 
						|
    {
 | 
						|
        return await db.PublisherSubscriptions
 | 
						|
            .AnyAsync(ps => ps.AccountId == accountId &&
 | 
						|
                            ps.PublisherId == publisherId &&
 | 
						|
                            ps.Status == PublisherSubscriptionStatus.Active);
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Gets a subscription by account and publisher ID
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="accountId">The account ID</param>
 | 
						|
    /// <param name="publisherId">The publisher ID</param>
 | 
						|
    /// <returns>The subscription or null if not found</returns>
 | 
						|
    public async Task<SnPublisherSubscription?> GetSubscriptionAsync(Guid accountId, Guid publisherId)
 | 
						|
    {
 | 
						|
        return await db.PublisherSubscriptions
 | 
						|
            .Include(ps => ps.Publisher)
 | 
						|
            .FirstOrDefaultAsync(ps => ps.AccountId == accountId && ps.PublisherId == publisherId);
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Notifies all subscribers about a new post from a publisher
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="post">The new post</param>
 | 
						|
    /// <returns>The number of subscribers notified</returns>
 | 
						|
    public async Task<int> NotifySubscriberPost(SnPost post)
 | 
						|
    {
 | 
						|
        if (post.RepliedPostId is not null)
 | 
						|
            return 0;
 | 
						|
        if (post.Visibility != Shared.Models.PostVisibility.Public)
 | 
						|
            return 0;
 | 
						|
        
 | 
						|
        // Create notification data
 | 
						|
        var (title, message) = ps.ChopPostForNotification(post);
 | 
						|
 | 
						|
        // Data to include with the notification
 | 
						|
        var data = new Dictionary<string, object>
 | 
						|
        {
 | 
						|
            { "post_id", post.Id.ToString() },
 | 
						|
            { "publisher_id", post.Publisher.Id.ToString() }
 | 
						|
        };
 | 
						|
 | 
						|
        // Gather subscribers
 | 
						|
        var subscribers = await db.PublisherSubscriptions
 | 
						|
            .Where(p => p.PublisherId == post.PublisherId &&
 | 
						|
                        p.Status == PublisherSubscriptionStatus.Active)
 | 
						|
            .ToListAsync();
 | 
						|
        if (subscribers.Count == 0)
 | 
						|
            return 0;
 | 
						|
 | 
						|
        List<SnPostCategorySubscription> categorySubscribers = [];
 | 
						|
        if (post.Categories.Count > 0)
 | 
						|
        {
 | 
						|
            var categoryIds = post.Categories.Select(x => x.Id).ToList();
 | 
						|
            var subs = await db.PostCategorySubscriptions
 | 
						|
                .Where(s => s.CategoryId != null && categoryIds.Contains(s.CategoryId.Value))
 | 
						|
                .ToListAsync();
 | 
						|
            categorySubscribers.AddRange(subs);
 | 
						|
        }
 | 
						|
        if (post.Tags.Count > 0)
 | 
						|
        {
 | 
						|
            var tagIds = post.Tags.Select(x => x.Id).ToList();
 | 
						|
            var subs = await db.PostCategorySubscriptions
 | 
						|
                .Where(s => s.TagId != null && tagIds.Contains(s.TagId.Value))
 | 
						|
                .ToListAsync();
 | 
						|
            categorySubscribers.AddRange(subs);
 | 
						|
        }
 | 
						|
 | 
						|
        List<string> requestAccountIds = [];
 | 
						|
        requestAccountIds.AddRange(subscribers.Select(x => x.AccountId.ToString()));
 | 
						|
        requestAccountIds.AddRange(categorySubscribers.Select(x => x.AccountId.ToString()));
 | 
						|
 | 
						|
        var queryRequest = new GetAccountBatchRequest();
 | 
						|
        queryRequest.Id.AddRange(requestAccountIds.Distinct());
 | 
						|
        var queryResponse = await accounts.GetAccountBatchAsync(queryRequest);
 | 
						|
 | 
						|
        // Notify each subscriber
 | 
						|
        var notifiedCount = 0;
 | 
						|
        foreach (var target in queryResponse.Accounts.GroupBy(x => x.Language))
 | 
						|
        {
 | 
						|
            try
 | 
						|
            {
 | 
						|
                CultureService.SetCultureInfo(target.Key);
 | 
						|
                var notification = new PushNotification
 | 
						|
                {
 | 
						|
                    Topic = "posts.new",
 | 
						|
                    Title = localizer["PostSubscriptionTitle", post.Publisher.Name, title],
 | 
						|
                    Body = message,
 | 
						|
                    Meta = GrpcTypeHelper.ConvertObjectToByteString(data),
 | 
						|
                    IsSavable = true,
 | 
						|
                    ActionUri = $"/posts/{post.Id}"
 | 
						|
                };
 | 
						|
                var request = new SendPushNotificationToUsersRequest { Notification = notification };
 | 
						|
                request.UserIds.AddRange(target.Select(x => x.Id.ToString()));
 | 
						|
                await pusher.SendPushNotificationToUsersAsync(request);
 | 
						|
                notifiedCount++;
 | 
						|
            }
 | 
						|
            catch (Exception)
 | 
						|
            {
 | 
						|
                // Log the error but continue with other notifications
 | 
						|
                // We don't want one failed notification to stop the others
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return notifiedCount;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Gets all active subscriptions for an account
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="accountId">The account ID</param>
 | 
						|
    /// <returns>A list of active subscriptions</returns>
 | 
						|
    public async Task<List<SnPublisherSubscription>> GetAccountSubscriptionsAsync(Guid accountId)
 | 
						|
    {
 | 
						|
        return await db.PublisherSubscriptions
 | 
						|
            .Include(ps => ps.Publisher)
 | 
						|
            .Where(ps => ps.AccountId == accountId && ps.Status == PublisherSubscriptionStatus.Active)
 | 
						|
            .ToListAsync();
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Gets all active subscribers for a publisher
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="publisherId">The publisher ID</param>
 | 
						|
    /// <returns>A list of active subscriptions</returns>
 | 
						|
    public async Task<List<SnPublisherSubscription>> GetPublisherSubscribersAsync(Guid publisherId)
 | 
						|
    {
 | 
						|
        return await db.PublisherSubscriptions
 | 
						|
            .Where(ps => ps.PublisherId == publisherId && ps.Status == PublisherSubscriptionStatus.Active)
 | 
						|
            .ToListAsync();
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Creates a new subscription between an account and a publisher
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="accountId">The account ID</param>
 | 
						|
    /// <param name="publisherId">The publisher ID</param>
 | 
						|
    /// <param name="tier">Optional subscription tier</param>
 | 
						|
    /// <returns>The created subscription</returns>
 | 
						|
    public async Task<SnPublisherSubscription> CreateSubscriptionAsync(
 | 
						|
        Guid accountId,
 | 
						|
        Guid publisherId,
 | 
						|
        int tier = 0
 | 
						|
    )
 | 
						|
    {
 | 
						|
        // Check if a subscription already exists
 | 
						|
        var existingSubscription = await GetSubscriptionAsync(accountId, publisherId);
 | 
						|
 | 
						|
        if (existingSubscription != null)
 | 
						|
        {
 | 
						|
            // If it exists but is not active, reactivate it
 | 
						|
            if (existingSubscription.Status == PublisherSubscriptionStatus.Active) return existingSubscription;
 | 
						|
            existingSubscription.Status = PublisherSubscriptionStatus.Active;
 | 
						|
            existingSubscription.Tier = tier;
 | 
						|
 | 
						|
            await db.SaveChangesAsync();
 | 
						|
            return existingSubscription;
 | 
						|
 | 
						|
            // If it's already active, just return it
 | 
						|
        }
 | 
						|
 | 
						|
        // Create a new subscription
 | 
						|
        var subscription = new SnPublisherSubscription
 | 
						|
        {
 | 
						|
            AccountId = accountId,
 | 
						|
            PublisherId = publisherId,
 | 
						|
            Status = PublisherSubscriptionStatus.Active,
 | 
						|
            Tier = tier,
 | 
						|
        };
 | 
						|
 | 
						|
        db.PublisherSubscriptions.Add(subscription);
 | 
						|
        await db.SaveChangesAsync();
 | 
						|
 | 
						|
        await cache.RemoveAsync(string.Format(PublisherService.SubscribedPublishersCacheKey, accountId));
 | 
						|
 | 
						|
        return subscription;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Cancels a subscription
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="accountId">The account ID</param>
 | 
						|
    /// <param name="publisherId">The publisher ID</param>
 | 
						|
    /// <returns>True if the subscription was cancelled, false if it wasn't found</returns>
 | 
						|
    public async Task<bool> CancelSubscriptionAsync(Guid accountId, Guid publisherId)
 | 
						|
    {
 | 
						|
        var subscription = await GetSubscriptionAsync(accountId, publisherId);
 | 
						|
        if (subscription is not { Status: PublisherSubscriptionStatus.Active })
 | 
						|
            return false;
 | 
						|
 | 
						|
        subscription.Status = PublisherSubscriptionStatus.Cancelled;
 | 
						|
        await db.SaveChangesAsync();
 | 
						|
 | 
						|
        await cache.RemoveAsync(string.Format(PublisherService.SubscribedPublishersCacheKey, accountId));
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
}
 |