650 lines
24 KiB
C#
650 lines
24 KiB
C#
using System.Globalization;
|
|
using DysonNetwork.Shared.Cache;
|
|
using DysonNetwork.Shared.Models;
|
|
using DysonNetwork.Shared.Proto;
|
|
using DysonNetwork.Shared.Registry;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NodaTime;
|
|
using NodaTime.Serialization.Protobuf;
|
|
using PublisherMemberRole = DysonNetwork.Shared.Models.PublisherMemberRole;
|
|
using PublisherType = DysonNetwork.Shared.Models.PublisherType;
|
|
|
|
namespace DysonNetwork.Sphere.Publisher;
|
|
|
|
public class PublisherService(
|
|
AppDatabase db,
|
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
|
SocialCreditService.SocialCreditServiceClient socialCredits,
|
|
ExperienceService.ExperienceServiceClient experiences,
|
|
ICacheService cache,
|
|
RemoteAccountService remoteAccounts
|
|
)
|
|
{
|
|
public async Task<SnPublisher?> GetPublisherByName(string name)
|
|
{
|
|
return await db.Publishers
|
|
.Where(e => e.Name == name)
|
|
.FirstOrDefaultAsync();
|
|
}
|
|
|
|
private const string UserPublishersCacheKey = "accounts:{0}:publishers";
|
|
|
|
public async Task<List<SnPublisher>> GetUserPublishers(Guid userId)
|
|
{
|
|
var cacheKey = string.Format(UserPublishersCacheKey, userId);
|
|
|
|
// Try to get publishers from the cache first
|
|
var publishers = await cache.GetAsync<List<SnPublisher>>(cacheKey);
|
|
if (publishers is not null)
|
|
return publishers;
|
|
|
|
// If not in cache, fetch from a database
|
|
var publishersId = await db.PublisherMembers
|
|
.Where(p => p.AccountId == userId)
|
|
.Select(p => p.PublisherId)
|
|
.ToListAsync();
|
|
publishers = await db.Publishers
|
|
.Where(p => publishersId.Contains(p.Id))
|
|
.ToListAsync();
|
|
|
|
// Store in a cache for 5 minutes
|
|
await cache.SetAsync(cacheKey, publishers, TimeSpan.FromMinutes(5));
|
|
|
|
return publishers;
|
|
}
|
|
|
|
public async Task<Dictionary<Guid, List<SnPublisher>>> GetUserPublishersBatch(List<Guid> userIds)
|
|
{
|
|
var result = new Dictionary<Guid, List<SnPublisher>>();
|
|
var missingIds = new List<Guid>();
|
|
|
|
// Try to get publishers from cache for each user
|
|
foreach (var userId in userIds)
|
|
{
|
|
var cacheKey = string.Format(UserPublishersCacheKey, userId);
|
|
var publishers = await cache.GetAsync<List<SnPublisher>>(cacheKey);
|
|
if (publishers != null)
|
|
result[userId] = publishers;
|
|
else
|
|
missingIds.Add(userId);
|
|
}
|
|
|
|
if (missingIds.Count <= 0) return result;
|
|
{
|
|
// Fetch missing data from database
|
|
var publisherMembers = await db.PublisherMembers
|
|
.Where(p => missingIds.Contains(p.AccountId))
|
|
.Select(p => new { p.AccountId, p.PublisherId })
|
|
.ToListAsync();
|
|
|
|
var publisherIds = publisherMembers.Select(p => p.PublisherId).Distinct().ToList();
|
|
var publishers = await db.Publishers
|
|
.Where(p => publisherIds.Contains(p.Id))
|
|
.ToListAsync();
|
|
|
|
// Group publishers by user id
|
|
foreach (var userId in missingIds)
|
|
{
|
|
var userPublisherIds = publisherMembers
|
|
.Where(p => p.AccountId == userId)
|
|
.Select(p => p.PublisherId)
|
|
.ToList();
|
|
|
|
var userPublishers = publishers
|
|
.Where(p => userPublisherIds.Contains(p.Id))
|
|
.ToList();
|
|
|
|
result[userId] = userPublishers;
|
|
|
|
// Cache individual results
|
|
var cacheKey = string.Format(UserPublishersCacheKey, userId);
|
|
await cache.SetAsync(cacheKey, userPublishers, TimeSpan.FromMinutes(5));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
public const string SubscribedPublishersCacheKey = "accounts:{0}:subscribed-publishers";
|
|
|
|
public async Task<List<SnPublisher>> GetSubscribedPublishers(Guid userId)
|
|
{
|
|
var cacheKey = string.Format(SubscribedPublishersCacheKey, userId);
|
|
|
|
// Try to get publishers from the cache first
|
|
var publishers = await cache.GetAsync<List<SnPublisher>>(cacheKey);
|
|
if (publishers is not null)
|
|
return publishers;
|
|
|
|
// If not in cache, fetch from a database
|
|
var publishersId = await db.PublisherSubscriptions
|
|
.Where(p => p.AccountId == userId)
|
|
.Where(p => p.Status == PublisherSubscriptionStatus.Active)
|
|
.Select(p => p.PublisherId)
|
|
.ToListAsync();
|
|
publishers = await db.Publishers
|
|
.Where(p => publishersId.Contains(p.Id))
|
|
.ToListAsync();
|
|
|
|
// Store in a cache for 5 minutes
|
|
await cache.SetAsync(cacheKey, publishers, TimeSpan.FromMinutes(5));
|
|
|
|
return publishers;
|
|
}
|
|
|
|
private const string PublisherMembersCacheKey = "publishers:{0}:members";
|
|
|
|
public async Task<List<SnPublisherMember>> GetPublisherMembers(Guid publisherId)
|
|
{
|
|
var cacheKey = string.Format(PublisherMembersCacheKey, publisherId);
|
|
|
|
// Try to get members from the cache first
|
|
var members = await cache.GetAsync<List<SnPublisherMember>>(cacheKey);
|
|
if (members is not null)
|
|
return members;
|
|
|
|
// If not in cache, fetch from a database
|
|
members = await db.PublisherMembers
|
|
.Where(p => p.PublisherId == publisherId)
|
|
.ToListAsync();
|
|
|
|
// Store in cache for 5 minutes (consistent with other cache durations in the class)
|
|
await cache.SetAsync(cacheKey, members, TimeSpan.FromMinutes(5));
|
|
|
|
return members;
|
|
}
|
|
|
|
public async Task<SnPublisher> CreateIndividualPublisher(
|
|
Account account,
|
|
string? name,
|
|
string? nick,
|
|
string? bio,
|
|
SnCloudFileReferenceObject? picture,
|
|
SnCloudFileReferenceObject? background
|
|
)
|
|
{
|
|
var publisher = new SnPublisher
|
|
{
|
|
Type = PublisherType.Individual,
|
|
Name = name ?? account.Name,
|
|
Nick = nick ?? account.Nick,
|
|
Bio = bio ?? account.Profile.Bio,
|
|
Picture = picture ?? (account.Profile.Picture is null
|
|
? null
|
|
: SnCloudFileReferenceObject.FromProtoValue(account.Profile.Picture)),
|
|
Background = background ?? (account.Profile.Background is null
|
|
? null
|
|
: SnCloudFileReferenceObject.FromProtoValue(account.Profile.Background)),
|
|
AccountId = Guid.Parse(account.Id),
|
|
Members =
|
|
[
|
|
new()
|
|
{
|
|
AccountId = Guid.Parse(account.Id),
|
|
Role = PublisherMemberRole.Owner,
|
|
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
|
|
}
|
|
]
|
|
};
|
|
|
|
db.Publishers.Add(publisher);
|
|
await db.SaveChangesAsync();
|
|
|
|
if (publisher.Picture is not null)
|
|
{
|
|
await fileRefs.CreateReferenceAsync(
|
|
new CreateReferenceRequest
|
|
{
|
|
FileId = publisher.Picture.Id,
|
|
Usage = "publisher.picture",
|
|
ResourceId = publisher.ResourceIdentifier,
|
|
}
|
|
);
|
|
}
|
|
|
|
if (publisher.Background is not null)
|
|
{
|
|
await fileRefs.CreateReferenceAsync(
|
|
new CreateReferenceRequest
|
|
{
|
|
FileId = publisher.Background.Id,
|
|
Usage = "publisher.background",
|
|
ResourceId = publisher.ResourceIdentifier,
|
|
}
|
|
);
|
|
}
|
|
|
|
return publisher;
|
|
}
|
|
|
|
public async Task<SnPublisher> CreateOrganizationPublisher(
|
|
SnRealm realm,
|
|
Account account,
|
|
string? name,
|
|
string? nick,
|
|
string? bio,
|
|
SnCloudFileReferenceObject? picture,
|
|
SnCloudFileReferenceObject? background
|
|
)
|
|
{
|
|
var publisher = new SnPublisher
|
|
{
|
|
Type = PublisherType.Organizational,
|
|
Name = name ?? realm.Slug,
|
|
Nick = nick ?? realm.Name,
|
|
Bio = bio ?? realm.Description,
|
|
Picture = picture ?? SnCloudFileReferenceObject.FromProtoValue(account.Profile.Picture),
|
|
Background = background ?? SnCloudFileReferenceObject.FromProtoValue(account.Profile.Background),
|
|
RealmId = realm.Id,
|
|
Members = new List<SnPublisherMember>
|
|
{
|
|
new()
|
|
{
|
|
AccountId = Guid.Parse(account.Id),
|
|
Role = PublisherMemberRole.Owner,
|
|
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
|
|
}
|
|
}
|
|
};
|
|
|
|
db.Publishers.Add(publisher);
|
|
await db.SaveChangesAsync();
|
|
|
|
if (publisher.Picture is not null)
|
|
{
|
|
await fileRefs.CreateReferenceAsync(
|
|
new CreateReferenceRequest
|
|
{
|
|
FileId = publisher.Picture.Id,
|
|
Usage = "publisher.picture",
|
|
ResourceId = publisher.ResourceIdentifier,
|
|
}
|
|
);
|
|
}
|
|
|
|
if (publisher.Background is not null)
|
|
{
|
|
await fileRefs.CreateReferenceAsync(
|
|
new CreateReferenceRequest
|
|
{
|
|
FileId = publisher.Background.Id,
|
|
Usage = "publisher.background",
|
|
ResourceId = publisher.ResourceIdentifier,
|
|
}
|
|
);
|
|
}
|
|
|
|
return publisher;
|
|
}
|
|
|
|
public class PublisherStats
|
|
{
|
|
public int PostsCreated { get; set; }
|
|
public int StickerPacksCreated { get; set; }
|
|
public int StickersCreated { get; set; }
|
|
public int UpvoteReceived { get; set; }
|
|
public int DownvoteReceived { get; set; }
|
|
public int SubscribersCount { get; set; }
|
|
}
|
|
|
|
private const string PublisherStatsCacheKey = "publisher:{0}:stats";
|
|
private const string PublisherHeatmapCacheKey = "publisher:{0}:heatmap";
|
|
private const string PublisherFeatureCacheKey = "publisher:{0}:feature:{1}";
|
|
|
|
public async Task<PublisherStats?> GetPublisherStats(string name)
|
|
{
|
|
var cacheKey = string.Format(PublisherStatsCacheKey, name);
|
|
var stats = await cache.GetAsync<PublisherStats>(cacheKey);
|
|
if (stats is not null)
|
|
return stats;
|
|
|
|
var publisher = await db.Publishers.FirstOrDefaultAsync(e => e.Name == name);
|
|
if (publisher is null) return null;
|
|
|
|
var postsCount = await db.Posts.Where(e => e.Publisher.Id == publisher.Id).CountAsync();
|
|
var postsUpvotes = await db.PostReactions
|
|
.Where(r => r.Post.Publisher.Id == publisher.Id &&
|
|
r.Attitude == Shared.Models.PostReactionAttitude.Positive)
|
|
.CountAsync();
|
|
var postsDownvotes = await db.PostReactions
|
|
.Where(r => r.Post.Publisher.Id == publisher.Id &&
|
|
r.Attitude == Shared.Models.PostReactionAttitude.Negative)
|
|
.CountAsync();
|
|
|
|
var stickerPacksId = await db.StickerPacks.Where(e => e.Publisher.Id == publisher.Id).Select(e => e.Id)
|
|
.ToListAsync();
|
|
var stickerPacksCount = stickerPacksId.Count;
|
|
|
|
var stickersCount = await db.Stickers.Where(e => stickerPacksId.Contains(e.PackId)).CountAsync();
|
|
|
|
var subscribersCount = await db.PublisherSubscriptions.Where(e => e.PublisherId == publisher.Id).CountAsync();
|
|
|
|
stats = new PublisherStats
|
|
{
|
|
PostsCreated = postsCount,
|
|
StickerPacksCreated = stickerPacksCount,
|
|
StickersCreated = stickersCount,
|
|
UpvoteReceived = postsUpvotes,
|
|
DownvoteReceived = postsDownvotes,
|
|
SubscribersCount = subscribersCount,
|
|
};
|
|
|
|
await cache.SetAsync(cacheKey, stats, TimeSpan.FromMinutes(5));
|
|
return stats;
|
|
}
|
|
|
|
public async Task<ActivityHeatmap?> GetPublisherHeatmap(string name)
|
|
{
|
|
var cacheKey = string.Format(PublisherHeatmapCacheKey, name);
|
|
var heatmap = await cache.GetAsync<ActivityHeatmap?>(cacheKey);
|
|
if (heatmap is not null)
|
|
return heatmap;
|
|
|
|
var publisher = await db.Publishers.FirstOrDefaultAsync(e => e.Name == name);
|
|
if (publisher is null) return null;
|
|
|
|
var now = SystemClock.Instance.GetCurrentInstant();
|
|
var periodStart = now.Minus(Duration.FromDays(365));
|
|
var periodEnd = now;
|
|
|
|
var postGroups = await db.Posts
|
|
.Where(p => p.PublisherId == publisher.Id && p.CreatedAt >= periodStart && p.CreatedAt <= periodEnd)
|
|
.Select(p => p.CreatedAt.InUtc().Date)
|
|
.GroupBy(d => d)
|
|
.Select(g => new { Date = g.Key, Count = g.Count() })
|
|
.ToListAsync();
|
|
|
|
var items = postGroups.Select(p => new ActivityHeatmapItem
|
|
{
|
|
Date = p.Date.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(),
|
|
Count = p.Count
|
|
}).ToList();
|
|
|
|
heatmap = new ActivityHeatmap
|
|
{
|
|
Unit = "posts",
|
|
PeriodStart = periodStart,
|
|
PeriodEnd = periodEnd,
|
|
Items = items.OrderBy(i => i.Date).ToList()
|
|
};
|
|
|
|
await cache.SetAsync(cacheKey, heatmap, TimeSpan.FromMinutes(5));
|
|
return heatmap;
|
|
}
|
|
|
|
public async Task SetFeatureFlag(Guid publisherId, string flag)
|
|
{
|
|
var featureFlag = await db.PublisherFeatures
|
|
.FirstOrDefaultAsync(f => f.PublisherId == publisherId && f.Flag == flag);
|
|
|
|
if (featureFlag == null)
|
|
{
|
|
featureFlag = new SnPublisherFeature
|
|
{
|
|
PublisherId = publisherId,
|
|
Flag = flag,
|
|
};
|
|
db.PublisherFeatures.Add(featureFlag);
|
|
}
|
|
else
|
|
{
|
|
featureFlag.ExpiredAt = SystemClock.Instance.GetCurrentInstant();
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
await cache.RemoveAsync(string.Format(PublisherFeatureCacheKey, publisherId, flag));
|
|
}
|
|
|
|
public async Task<bool> HasFeature(Guid publisherId, string flag)
|
|
{
|
|
var cacheKey = string.Format(PublisherFeatureCacheKey, publisherId, flag);
|
|
|
|
var isEnabled = await cache.GetAsync<bool?>(cacheKey);
|
|
if (isEnabled.HasValue)
|
|
return isEnabled.Value;
|
|
|
|
var now = SystemClock.Instance.GetCurrentInstant();
|
|
var featureFlag = await db.PublisherFeatures
|
|
.FirstOrDefaultAsync(f =>
|
|
f.PublisherId == publisherId && f.Flag == flag &&
|
|
(f.ExpiredAt == null || f.ExpiredAt > now)
|
|
);
|
|
isEnabled = featureFlag is not null;
|
|
|
|
await cache.SetAsync(cacheKey, isEnabled!.Value, TimeSpan.FromMinutes(5));
|
|
return isEnabled.Value;
|
|
}
|
|
|
|
public async Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId,
|
|
Shared.Models.PublisherMemberRole requiredRole)
|
|
{
|
|
var member = await db.Publishers
|
|
.Where(p => p.Id == publisherId)
|
|
.SelectMany(p => p.Members)
|
|
.FirstOrDefaultAsync(m => m.AccountId == accountId);
|
|
|
|
return member != null && member.Role >= requiredRole;
|
|
}
|
|
|
|
public async Task<SnPublisherMember> LoadMemberAccount(SnPublisherMember member)
|
|
{
|
|
var account = await remoteAccounts.GetAccount(member.AccountId);
|
|
member.Account = SnAccount.FromProtoValue(account);
|
|
return member;
|
|
}
|
|
|
|
public async Task<List<SnPublisherMember>> LoadMemberAccounts(ICollection<SnPublisherMember> members)
|
|
{
|
|
var accountIds = members.Select(m => m.AccountId).ToList();
|
|
var accounts = (await remoteAccounts.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
|
|
|
return
|
|
[
|
|
.. members.Select(m =>
|
|
{
|
|
if (accounts.TryGetValue(m.AccountId, out var account))
|
|
m.Account = SnAccount.FromProtoValue(account);
|
|
return m;
|
|
})
|
|
];
|
|
}
|
|
|
|
public async Task<List<SnPublisher>> LoadIndividualPublisherAccounts(ICollection<SnPublisher> publishers)
|
|
{
|
|
var accountIds = publishers
|
|
.Where(p => p.AccountId.HasValue && p.Type == PublisherType.Individual)
|
|
.Select(p => p.AccountId!.Value)
|
|
.ToList();
|
|
if (accountIds.Count == 0) return publishers.ToList();
|
|
|
|
var accounts = (await remoteAccounts.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
|
|
|
foreach (var p in publishers)
|
|
{
|
|
if (p.AccountId.HasValue && accounts.TryGetValue(p.AccountId.Value, out var account))
|
|
p.Account = SnAccount.FromProtoValue(account);
|
|
}
|
|
|
|
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(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|