✨ Publisher rewarding
This commit is contained in:
		@@ -9,7 +9,13 @@ public class SocialCreditService(AppDatabase db, ICacheService cache)
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    private const string CacheKeyPrefix = "account:credits:";
 | 
					    private const string CacheKeyPrefix = "account:credits:";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<SnSocialCreditRecord> AddRecord(string reasonType, string reason, double delta, Guid accountId)
 | 
					    public async Task<SnSocialCreditRecord> AddRecord(
 | 
				
			||||||
 | 
					        string reasonType,
 | 
				
			||||||
 | 
					        string reason,
 | 
				
			||||||
 | 
					        double delta,
 | 
				
			||||||
 | 
					        Guid accountId,
 | 
				
			||||||
 | 
					        Instant? expiredAt
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var record = new SnSocialCreditRecord
 | 
					        var record = new SnSocialCreditRecord
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -17,6 +23,7 @@ public class SocialCreditService(AppDatabase db, ICacheService cache)
 | 
				
			|||||||
            Reason = reason,
 | 
					            Reason = reason,
 | 
				
			||||||
            Delta = delta,
 | 
					            Delta = delta,
 | 
				
			||||||
            AccountId = accountId,
 | 
					            AccountId = accountId,
 | 
				
			||||||
 | 
					            ExpiredAt = expiredAt
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        db.SocialCreditRecords.Add(record);
 | 
					        db.SocialCreditRecords.Add(record);
 | 
				
			||||||
        await db.SaveChangesAsync();
 | 
					        await db.SaveChangesAsync();
 | 
				
			||||||
@@ -61,7 +68,8 @@ public class SocialCreditService(AppDatabase db, ICacheService cache)
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            await db.AccountProfiles
 | 
					            await db.AccountProfiles
 | 
				
			||||||
                .Where(p => p.AccountId == accountId)
 | 
					                .Where(p => p.AccountId == accountId)
 | 
				
			||||||
                .ExecuteUpdateAsync(p => p.SetProperty(v => v.SocialCredits, v => v.SocialCredits - totalDeltaSubtracted));
 | 
					                .ExecuteUpdateAsync(p =>
 | 
				
			||||||
 | 
					                    p.SetProperty(v => v.SocialCredits, v => v.SocialCredits - totalDeltaSubtracted));
 | 
				
			||||||
            await cache.RemoveAsync($"{CacheKeyPrefix}{accountId}");
 | 
					            await cache.RemoveAsync($"{CacheKeyPrefix}{accountId}");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,29 @@
 | 
				
			|||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using Grpc.Core;
 | 
					using Grpc.Core;
 | 
				
			||||||
 | 
					using NodaTime.Serialization.Protobuf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace DysonNetwork.Pass.Credit;
 | 
					namespace DysonNetwork.Pass.Credit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class SocialCreditServiceGrpc(SocialCreditService creditService) : Shared.Proto.SocialCreditService.SocialCreditServiceBase
 | 
					public class SocialCreditServiceGrpc(SocialCreditService creditService)
 | 
				
			||||||
 | 
					    : Shared.Proto.SocialCreditService.SocialCreditServiceBase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public override async Task<Shared.Proto.SocialCreditRecord> AddRecord(AddSocialCreditRecordRequest request, ServerCallContext context)
 | 
					    public override async Task<SocialCreditRecord> AddRecord(AddSocialCreditRecordRequest request,
 | 
				
			||||||
 | 
					        ServerCallContext context)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var accountId = Guid.Parse(request.AccountId);
 | 
					        var accountId = Guid.Parse(request.AccountId);
 | 
				
			||||||
        var record = await creditService.AddRecord(
 | 
					        var record = await creditService.AddRecord(
 | 
				
			||||||
            request.ReasonType,
 | 
					            request.ReasonType,
 | 
				
			||||||
            request.Reason,
 | 
					            request.Reason,
 | 
				
			||||||
            request.Delta,
 | 
					            request.Delta,
 | 
				
			||||||
            accountId);
 | 
					            accountId,
 | 
				
			||||||
 | 
					            request.ExpiredAt.ToInstant()
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return record.ToProto();
 | 
					        return record.ToProto();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override async Task<SocialCreditResponse> GetSocialCredit(GetSocialCreditRequest request, ServerCallContext context)
 | 
					    public override async Task<SocialCreditResponse> GetSocialCredit(GetSocialCreditRequest request,
 | 
				
			||||||
 | 
					        ServerCallContext context)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var accountId = Guid.Parse(request.AccountId);
 | 
					        var accountId = Guid.Parse(request.AccountId);
 | 
				
			||||||
        var amount = await creditService.GetSocialCredit(accountId);
 | 
					        var amount = await creditService.GetSocialCredit(accountId);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,25 +14,25 @@ import "google/protobuf/field_mask.proto";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// SocialCreditRecord represents a record of social credit changes for an account
 | 
					// SocialCreditRecord represents a record of social credit changes for an account
 | 
				
			||||||
message SocialCreditRecord {
 | 
					message SocialCreditRecord {
 | 
				
			||||||
  string id = 1; // UUID string
 | 
					    string id = 1; // UUID string
 | 
				
			||||||
  string reason_type = 2;
 | 
					    string reason_type = 2;
 | 
				
			||||||
  string reason = 3;
 | 
					    string reason = 3;
 | 
				
			||||||
  double delta = 4;
 | 
					    double delta = 4;
 | 
				
			||||||
  string account_id = 5; // UUID string
 | 
					    string account_id = 5; // UUID string
 | 
				
			||||||
  google.protobuf.Timestamp created_at = 6;
 | 
					    google.protobuf.Timestamp created_at = 6;
 | 
				
			||||||
  google.protobuf.Timestamp updated_at = 7;
 | 
					    google.protobuf.Timestamp updated_at = 7;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ExperienceRecord represents a record of experience points gained by an account
 | 
					// ExperienceRecord represents a record of experience points gained by an account
 | 
				
			||||||
message ExperienceRecord {
 | 
					message ExperienceRecord {
 | 
				
			||||||
  string id = 1; // UUID string
 | 
					    string id = 1; // UUID string
 | 
				
			||||||
  string reason_type = 2;
 | 
					    string reason_type = 2;
 | 
				
			||||||
  string reason = 3;
 | 
					    string reason = 3;
 | 
				
			||||||
  int64 delta = 4;
 | 
					    int64 delta = 4;
 | 
				
			||||||
  double bonus_multiplier = 5;
 | 
					    double bonus_multiplier = 5;
 | 
				
			||||||
  string account_id = 6; // UUID string
 | 
					    string account_id = 6; // UUID string
 | 
				
			||||||
  google.protobuf.Timestamp created_at = 7;
 | 
					    google.protobuf.Timestamp created_at = 7;
 | 
				
			||||||
  google.protobuf.Timestamp updated_at = 8;
 | 
					    google.protobuf.Timestamp updated_at = 8;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ====================================
 | 
					// ====================================
 | 
				
			||||||
@@ -41,26 +41,27 @@ message ExperienceRecord {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Social Credit Requests/Responses
 | 
					// Social Credit Requests/Responses
 | 
				
			||||||
message AddSocialCreditRecordRequest {
 | 
					message AddSocialCreditRecordRequest {
 | 
				
			||||||
  string reason_type = 1;
 | 
					    string reason_type = 1;
 | 
				
			||||||
  string reason = 2;
 | 
					    string reason = 2;
 | 
				
			||||||
  double delta = 3;
 | 
					    double delta = 3;
 | 
				
			||||||
  string account_id = 4; // UUID string
 | 
					    string account_id = 4; // UUID string
 | 
				
			||||||
 | 
					    google.protobuf.Timestamp expired_at = 5;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
message GetSocialCreditRequest {
 | 
					message GetSocialCreditRequest {
 | 
				
			||||||
  string account_id = 1; // UUID string
 | 
					    string account_id = 1; // UUID string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
message SocialCreditResponse {
 | 
					message SocialCreditResponse {
 | 
				
			||||||
  double amount = 1;
 | 
					    double amount = 1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Experience Requests/Responses
 | 
					// Experience Requests/Responses
 | 
				
			||||||
message AddExperienceRecordRequest {
 | 
					message AddExperienceRecordRequest {
 | 
				
			||||||
  string reason_type = 1;
 | 
					    string reason_type = 1;
 | 
				
			||||||
  string reason = 2;
 | 
					    string reason = 2;
 | 
				
			||||||
  int64 delta = 3;
 | 
					    int64 delta = 3;
 | 
				
			||||||
  string account_id = 4; // UUID string
 | 
					    string account_id = 4; // UUID string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ====================================
 | 
					// ====================================
 | 
				
			||||||
@@ -69,15 +70,15 @@ message AddExperienceRecordRequest {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// SocialCreditService provides operations for managing social credit scores
 | 
					// SocialCreditService provides operations for managing social credit scores
 | 
				
			||||||
service SocialCreditService {
 | 
					service SocialCreditService {
 | 
				
			||||||
  // Adds a new social credit record for an account
 | 
					    // Adds a new social credit record for an account
 | 
				
			||||||
  rpc AddRecord(AddSocialCreditRecordRequest) returns (SocialCreditRecord);
 | 
					    rpc AddRecord(AddSocialCreditRecordRequest) returns (SocialCreditRecord);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Gets the current social credit score for an account
 | 
					    // Gets the current social credit score for an account
 | 
				
			||||||
  rpc GetSocialCredit(GetSocialCreditRequest) returns (SocialCreditResponse);
 | 
					    rpc GetSocialCredit(GetSocialCreditRequest) returns (SocialCreditResponse);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ExperienceService provides operations for managing experience points
 | 
					// ExperienceService provides operations for managing experience points
 | 
				
			||||||
service ExperienceService {
 | 
					service ExperienceService {
 | 
				
			||||||
  // Adds a new experience record for an account
 | 
					    // Adds a new experience record for an account
 | 
				
			||||||
  rpc AddRecord(AddExperienceRecordRequest) returns (ExperienceRecord);
 | 
					    rpc AddRecord(AddExperienceRecordRequest) returns (ExperienceRecord);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,6 +72,18 @@ public static class ServiceInjectionHelper
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
        services.AddSingleton<RemoteRealmService>();
 | 
					        services.AddSingleton<RemoteRealmService>();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					        services
 | 
				
			||||||
 | 
					            .AddGrpcClient<SocialCreditService.SocialCreditServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
 | 
				
			||||||
 | 
					            .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
 | 
				
			||||||
 | 
					                { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        services
 | 
				
			||||||
 | 
					            .AddGrpcClient<ExperienceService.ExperienceServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
 | 
				
			||||||
 | 
					            .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
 | 
				
			||||||
 | 
					                { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return services;
 | 
					        return services;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authorization;
 | 
				
			|||||||
using Microsoft.AspNetCore.Mvc;
 | 
					using Microsoft.AspNetCore.Mvc;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					using PublisherMemberRole = DysonNetwork.Shared.Models.PublisherMemberRole;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace DysonNetwork.Sphere.Publisher;
 | 
					namespace DysonNetwork.Sphere.Publisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -650,6 +651,27 @@ public class PublisherController(
 | 
				
			|||||||
        return Ok(dict);
 | 
					        return Ok(dict);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [HttpGet("{name}/rewards")]
 | 
				
			||||||
 | 
					    [Authorize]
 | 
				
			||||||
 | 
					    public async Task<ActionResult<PublisherService.PublisherRewardPreview>> GetPublisherExpectedReward(
 | 
				
			||||||
 | 
					        string name
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
				
			||||||
 | 
					        var accountId = Guid.Parse(currentUser.Id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var publisher = await db.Publishers
 | 
				
			||||||
 | 
					            .Where(p => p.Name == name)
 | 
				
			||||||
 | 
					            .FirstOrDefaultAsync();
 | 
				
			||||||
 | 
					        if (publisher is null) return NotFound();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!await ps.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Viewer))
 | 
				
			||||||
 | 
					            return StatusCode(403, "You are not allowed to view stats data of this publisher.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var result = await ps.GetPublisherExpectedReward(publisher.Id);
 | 
				
			||||||
 | 
					        return Ok(result);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public class PublisherFeatureRequest
 | 
					    public class PublisherFeatureRequest
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [Required] public string Flag { get; set; } = null!;
 | 
					        [Required] public string Flag { get; set; } = null!;
 | 
				
			||||||
@@ -701,4 +723,13 @@ public class PublisherController(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return NoContent();
 | 
					        return NoContent();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [HttpPost("rewards/settle")]
 | 
				
			||||||
 | 
					    [Authorize]
 | 
				
			||||||
 | 
					    [RequiredPermission("maintenance", "publishers.reward.settle")]
 | 
				
			||||||
 | 
					    public async Task<IActionResult> PerformLotteryDraw()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        await ps.SettlePublisherRewards();
 | 
				
			||||||
 | 
					        return Ok();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,9 +1,11 @@
 | 
				
			|||||||
 | 
					using System.Globalization;
 | 
				
			||||||
using DysonNetwork.Shared.Cache;
 | 
					using DysonNetwork.Shared.Cache;
 | 
				
			||||||
using DysonNetwork.Shared.Models;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using DysonNetwork.Shared.Registry;
 | 
					using DysonNetwork.Shared.Registry;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					using NodaTime.Serialization.Protobuf;
 | 
				
			||||||
using PublisherMemberRole = DysonNetwork.Shared.Models.PublisherMemberRole;
 | 
					using PublisherMemberRole = DysonNetwork.Shared.Models.PublisherMemberRole;
 | 
				
			||||||
using PublisherType = DysonNetwork.Shared.Models.PublisherType;
 | 
					using PublisherType = DysonNetwork.Shared.Models.PublisherType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,6 +14,8 @@ namespace DysonNetwork.Sphere.Publisher;
 | 
				
			|||||||
public class PublisherService(
 | 
					public class PublisherService(
 | 
				
			||||||
    AppDatabase db,
 | 
					    AppDatabase db,
 | 
				
			||||||
    FileReferenceService.FileReferenceServiceClient fileRefs,
 | 
					    FileReferenceService.FileReferenceServiceClient fileRefs,
 | 
				
			||||||
 | 
					    SocialCreditService.SocialCreditServiceClient socialCredits,
 | 
				
			||||||
 | 
					    ExperienceService.ExperienceServiceClient experiences,
 | 
				
			||||||
    ICacheService cache,
 | 
					    ICacheService cache,
 | 
				
			||||||
    RemoteAccountService remoteAccounts
 | 
					    RemoteAccountService remoteAccounts
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -300,10 +304,12 @@ public class PublisherService(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        var postsCount = await db.Posts.Where(e => e.Publisher.Id == publisher.Id).CountAsync();
 | 
					        var postsCount = await db.Posts.Where(e => e.Publisher.Id == publisher.Id).CountAsync();
 | 
				
			||||||
        var postsUpvotes = await db.PostReactions
 | 
					        var postsUpvotes = await db.PostReactions
 | 
				
			||||||
            .Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == Shared.Models.PostReactionAttitude.Positive)
 | 
					            .Where(r => r.Post.Publisher.Id == publisher.Id &&
 | 
				
			||||||
 | 
					                        r.Attitude == Shared.Models.PostReactionAttitude.Positive)
 | 
				
			||||||
            .CountAsync();
 | 
					            .CountAsync();
 | 
				
			||||||
        var postsDownvotes = await db.PostReactions
 | 
					        var postsDownvotes = await db.PostReactions
 | 
				
			||||||
            .Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == Shared.Models.PostReactionAttitude.Negative)
 | 
					            .Where(r => r.Post.Publisher.Id == publisher.Id &&
 | 
				
			||||||
 | 
					                        r.Attitude == Shared.Models.PostReactionAttitude.Negative)
 | 
				
			||||||
            .CountAsync();
 | 
					            .CountAsync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var stickerPacksId = await db.StickerPacks.Where(e => e.Publisher.Id == publisher.Id).Select(e => e.Id)
 | 
					        var stickerPacksId = await db.StickerPacks.Where(e => e.Publisher.Id == publisher.Id).Select(e => e.Id)
 | 
				
			||||||
@@ -462,4 +468,182 @@ public class PublisherService(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return publishers.ToList();
 | 
					        return publishers.ToList();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public class PublisherRewardPreview
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        public int Experience { get; set; }
 | 
				
			||||||
 | 
					        public int SocialCredits { get; set; }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async Task<PublisherRewardPreview> GetPublisherExpectedReward(Guid publisherId)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var cacheKey = $"publisher:{publisherId}:rewards";
 | 
				
			||||||
 | 
					        var (found, cached) = await cache.GetAsyncWithStatus<PublisherRewardPreview>(cacheKey);
 | 
				
			||||||
 | 
					        if (found)
 | 
				
			||||||
 | 
					            return cached!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var now = SystemClock.Instance.GetCurrentInstant();
 | 
				
			||||||
 | 
					        var yesterday = now.InZone(DateTimeZone.Utc).Date.PlusDays(-1);
 | 
				
			||||||
 | 
					        var periodStart = yesterday.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
 | 
				
			||||||
 | 
					        var periodEnd = periodStart.Plus(Duration.FromDays(1)).Minus(Duration.FromMilliseconds(1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get posts stats for this publisher: count, id, exclude content
 | 
				
			||||||
 | 
					        var postsInPeriod = await db.Posts
 | 
				
			||||||
 | 
					            .Where(p => p.PublisherId == publisherId && p.CreatedAt >= periodStart && p.CreatedAt <= periodEnd)
 | 
				
			||||||
 | 
					            .Select(p => new { Id = p.Id, AwardedScore = p.AwardedScore })
 | 
				
			||||||
 | 
					            .ToListAsync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get reactions for these posts
 | 
				
			||||||
 | 
					        var postIds = postsInPeriod.Select(p => p.Id).ToList();
 | 
				
			||||||
 | 
					        var reactions = await db.PostReactions
 | 
				
			||||||
 | 
					            .Where(r => postIds.Contains(r.PostId))
 | 
				
			||||||
 | 
					            .ToListAsync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (postsInPeriod.Count == 0)
 | 
				
			||||||
 | 
					            return new PublisherRewardPreview { Experience = 0, SocialCredits = 0 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Calculate stats
 | 
				
			||||||
 | 
					        var postCount = postsInPeriod.Count;
 | 
				
			||||||
 | 
					        var upvotes = reactions.Count(r => r.Attitude == Shared.Models.PostReactionAttitude.Positive);
 | 
				
			||||||
 | 
					        var downvotes = reactions.Count(r => r.Attitude == Shared.Models.PostReactionAttitude.Negative);
 | 
				
			||||||
 | 
					        var awardScore = postsInPeriod.Sum(p => (double)p.AwardedScore);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Each post counts as 100 experiences,
 | 
				
			||||||
 | 
					        // and each point (upvote - downvote + award score * 0.1) count as 10 experiences
 | 
				
			||||||
 | 
					        var netVotes = upvotes - downvotes;
 | 
				
			||||||
 | 
					        var points = netVotes + awardScore * 0.1;
 | 
				
			||||||
 | 
					        var experienceFromPosts = postCount * 100;
 | 
				
			||||||
 | 
					        var experienceFromPoints = (int)(points * 10);
 | 
				
			||||||
 | 
					        var totalExperience = experienceFromPosts + experienceFromPoints;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var preview = new PublisherRewardPreview
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Experience = totalExperience,
 | 
				
			||||||
 | 
					            SocialCredits = (int)(points * 10)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await cache.SetAsync(cacheKey, preview, TimeSpan.FromMinutes(5));
 | 
				
			||||||
 | 
					        return preview;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async Task SettlePublisherRewards()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var now = SystemClock.Instance.GetCurrentInstant();
 | 
				
			||||||
 | 
					        var yesterday = now.InZone(DateTimeZone.Utc).Date.PlusDays(-1);
 | 
				
			||||||
 | 
					        var periodStart = yesterday.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
 | 
				
			||||||
 | 
					        var periodEnd = periodStart.Plus(Duration.FromDays(1)).Minus(Duration.FromMilliseconds(1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get posts stats: count, publisher id, exclude content
 | 
				
			||||||
 | 
					        var postsInPeriod = await db.Posts
 | 
				
			||||||
 | 
					            .Where(p => p.CreatedAt >= periodStart && p.CreatedAt <= periodEnd)
 | 
				
			||||||
 | 
					            .Select(p => new { Id = p.Id, PublisherId = p.PublisherId, AwardedScore = p.AwardedScore })
 | 
				
			||||||
 | 
					            .ToListAsync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get reactions for these posts
 | 
				
			||||||
 | 
					        var postIds = postsInPeriod.Select(p => p.Id).ToList();
 | 
				
			||||||
 | 
					        var reactions = await db.PostReactions
 | 
				
			||||||
 | 
					            .Where(r => postIds.Contains(r.PostId))
 | 
				
			||||||
 | 
					            .ToListAsync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Group stats by publisher id
 | 
				
			||||||
 | 
					        var postIdToPublisher = postsInPeriod.ToDictionary(p => p.Id, p => p.PublisherId);
 | 
				
			||||||
 | 
					        var publisherStats = postsInPeriod
 | 
				
			||||||
 | 
					            .GroupBy(p => p.PublisherId)
 | 
				
			||||||
 | 
					            .ToDictionary(g => g.Key,
 | 
				
			||||||
 | 
					                g => new
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    PostCount = g.Count(), Upvotes = 0, Downvotes = 0, AwardScore = g.Sum(p => (double)p.AwardedScore)
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach (var reaction in reactions.Where(r => r.Attitude == Shared.Models.PostReactionAttitude.Positive))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (!postIdToPublisher.TryGetValue(reaction.PostId, out var pubId) ||
 | 
				
			||||||
 | 
					                !publisherStats.TryGetValue(pubId, out var stat)) continue;
 | 
				
			||||||
 | 
					            stat = new { stat.PostCount, Upvotes = stat.Upvotes + 1, stat.Downvotes, stat.AwardScore };
 | 
				
			||||||
 | 
					            publisherStats[pubId] = stat;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach (var reaction in reactions.Where(r => r.Attitude == Shared.Models.PostReactionAttitude.Negative))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (!postIdToPublisher.TryGetValue(reaction.PostId, out var pubId) ||
 | 
				
			||||||
 | 
					                !publisherStats.TryGetValue(pubId, out var stat)) continue;
 | 
				
			||||||
 | 
					            stat = new { stat.PostCount, stat.Upvotes, Downvotes = stat.Downvotes + 1, stat.AwardScore };
 | 
				
			||||||
 | 
					            publisherStats[pubId] = stat;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var date = now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var publisherIds = publisherStats.Keys.ToList();
 | 
				
			||||||
 | 
					        var publisherMembers = await db.PublisherMembers
 | 
				
			||||||
 | 
					            .Where(m => publisherIds.Contains(m.PublisherId))
 | 
				
			||||||
 | 
					            .ToListAsync();
 | 
				
			||||||
 | 
					        var accountIds = publisherMembers.Select(m => m.AccountId).ToList();
 | 
				
			||||||
 | 
					        var accounts = (await remoteAccounts.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
 | 
				
			||||||
 | 
					        var publisherAccounts = publisherMembers
 | 
				
			||||||
 | 
					            .GroupBy(m => m.PublisherId)
 | 
				
			||||||
 | 
					            .ToDictionary(g => g.Key, g => g.Select(m => SnAccount.FromProtoValue(accounts[m.AccountId])).ToList());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Foreach loop through publishers to calculate experience
 | 
				
			||||||
 | 
					        foreach (var (publisherId, value) in publisherStats)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var postCount = value.PostCount;
 | 
				
			||||||
 | 
					            var upvotes = value.Upvotes;
 | 
				
			||||||
 | 
					            var downvotes = value.Downvotes;
 | 
				
			||||||
 | 
					            var awardScore = value.AwardScore; // Fetch or calculate here
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Each post counts as 100 experiences,
 | 
				
			||||||
 | 
					            // and each point (upvote - downvote + award score * 0.1) count as 10 experiences
 | 
				
			||||||
 | 
					            var netVotes = upvotes - downvotes;
 | 
				
			||||||
 | 
					            var points = netVotes + awardScore * 0.1;
 | 
				
			||||||
 | 
					            var experienceFromPosts = postCount * 100;
 | 
				
			||||||
 | 
					            var experienceFromPoints = (int)(points * 10);
 | 
				
			||||||
 | 
					            var totalExperience = experienceFromPosts + experienceFromPoints;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!publisherAccounts.TryGetValue(publisherId, out var receivers) || receivers.Count == 0)
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Use totalExperience for rewarding
 | 
				
			||||||
 | 
					            foreach (var receiver in receivers)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                await experiences.AddRecordAsync(new AddExperienceRecordRequest
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Reason = $"Publishing Reward on {date}",
 | 
				
			||||||
 | 
					                    ReasonType = "publishers.rewards",
 | 
				
			||||||
 | 
					                    AccountId = receiver.Id.ToString(),
 | 
				
			||||||
 | 
					                    Delta = totalExperience,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Foreach loop through publishers to set social credit
 | 
				
			||||||
 | 
					        var expiredAt = now.InZone(DateTimeZone.Utc).Date.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).Minus(Duration.FromMilliseconds(1)).ToInstant();
 | 
				
			||||||
 | 
					        foreach (var (publisherId, value) in publisherStats)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var upvotes = value.Upvotes;
 | 
				
			||||||
 | 
					            var downvotes = value.Downvotes;
 | 
				
			||||||
 | 
					            var awardScore = value.AwardScore; // Fetch or calculate here
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var netVotes = upvotes - downvotes;
 | 
				
			||||||
 | 
					            var points = netVotes + awardScore * 0.1;
 | 
				
			||||||
 | 
					            var socialCreditDelta = (int)(points * 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (socialCreditDelta == 0) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!publisherAccounts.TryGetValue(publisherId, out var receivers) || receivers.Count == 0)
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Set social credit for receivers, expired before next settle
 | 
				
			||||||
 | 
					            foreach (var receiver in receivers)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                await socialCredits.AddRecordAsync(new AddSocialCreditRecordRequest
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Reason = $"Publishing Reward on {date}",
 | 
				
			||||||
 | 
					                    ReasonType = "publishers.rewards",
 | 
				
			||||||
 | 
					                    AccountId = receiver.Id.ToString(),
 | 
				
			||||||
 | 
					                    Delta = socialCreditDelta,
 | 
				
			||||||
 | 
					                    ExpiredAt = expiredAt.ToTimestamp(),
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								DysonNetwork.Sphere/Publisher/PublisherSettlementJob.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								DysonNetwork.Sphere/Publisher/PublisherSettlementJob.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					using Quartz;
 | 
				
			||||||
 | 
					using DysonNetwork.Sphere.Publisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace DysonNetwork.Sphere.Publisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class PublisherSettlementJob(PublisherService publisherService) : IJob
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public async Task Execute(IJobExecutionContext context)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        await publisherService.SettlePublisherRewards();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
using DysonNetwork.Sphere.Post;
 | 
					using DysonNetwork.Sphere.Post;
 | 
				
			||||||
 | 
					using DysonNetwork.Sphere.Publisher;
 | 
				
			||||||
using DysonNetwork.Sphere.WebReader;
 | 
					using DysonNetwork.Sphere.WebReader;
 | 
				
			||||||
using Quartz;
 | 
					using Quartz;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,6 +32,13 @@ public static class ScheduledJobsConfiguration
 | 
				
			|||||||
                .WithIdentity("WebFeedScraperTrigger")
 | 
					                .WithIdentity("WebFeedScraperTrigger")
 | 
				
			||||||
                .WithCronSchedule("0 0 0 * * ?")
 | 
					                .WithCronSchedule("0 0 0 * * ?")
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            q.AddJob<PublisherSettlementJob>(opts => opts.WithIdentity("PublisherSettlement"));
 | 
				
			||||||
 | 
					            q.AddTrigger(opts => opts
 | 
				
			||||||
 | 
					                .ForJob("PublisherSettlement")
 | 
				
			||||||
 | 
					                .WithIdentity("PublisherSettlementTrigger")
 | 
				
			||||||
 | 
					                .WithCronSchedule("0 0 0 * * ?")
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
 | 
					        services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user