Skip to content

Commit

Permalink
Meta + Voting
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Mar 23, 2024
1 parent 5c7a8d0 commit 71cc4a5
Show file tree
Hide file tree
Showing 23 changed files with 791 additions and 163 deletions.
30 changes: 26 additions & 4 deletions MyApp.ServiceInterface/BackgroundMqServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,40 @@ public async Task Any(DiskTasks request)
}
}

public async Task Any(DbWriteTasks request)
{
var vote = request.RecordPostVote;
if (vote != null)
{
if (string.IsNullOrEmpty(vote.RefId))
throw new ArgumentNullException(nameof(vote.RefId));
if (string.IsNullOrEmpty(vote.UserName))
throw new ArgumentNullException(nameof(vote.UserName));

await Db.DeleteAsync<Vote>(new { vote.RefId, vote.UserName });
if (vote.Score != 0)
{
await Db.InsertAsync(vote);
}

MessageProducer.Publish(new RenderComponent {
RegenerateMeta = vote.PostId
});
}
}

public async Task Any(AnalyticsTasks request)
{
if (request.RecordPostStat != null && !Stats.IsAdminOrModerator(request.RecordPostStat.UserName))
if (request.RecordPostView != null && !Stats.IsAdminOrModerator(request.RecordPostView.UserName))
{
using var analyticsDb = HostContext.AppHost.GetDbConnection(Databases.Analytics);
await analyticsDb.InsertAsync(request.RecordPostStat);
await analyticsDb.InsertAsync(request.RecordPostView);
}

if (request.RecordSearchStat != null && !Stats.IsAdminOrModerator(request.RecordSearchStat.UserName))
if (request.RecordSearchView != null && !Stats.IsAdminOrModerator(request.RecordSearchView.UserName))
{
using var analyticsDb = HostContext.AppHost.GetDbConnection(Databases.Analytics);
await analyticsDb.InsertAsync(request.RecordSearchStat);
await analyticsDb.InsertAsync(request.RecordSearchView);
}
}
}
97 changes: 63 additions & 34 deletions MyApp.ServiceInterface/Data/QuestionFiles.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using System.Collections.Concurrent;
using System.Data;
using MyApp.ServiceModel;
using ServiceStack;
using ServiceStack.IO;
using ServiceStack.OrmLite;
using ServiceStack.Text;

namespace MyApp.Data;
Expand All @@ -25,18 +23,55 @@ public class QuestionFiles(int id, string dir1, string dir2, string fileId, List
["accepted"] = 9,
["most-voted"] = 10,
};

public int GetModelScore(string model) => model switch {
"accepted" => AcceptedScore,
"most-voted" => MostVotedScore,
_ => ModelScores.GetValueOrDefault(model, 0)
};

public int Id { get; init; } = id;
public string Dir1 { get; init; } = dir1;
public string Dir2 { get; init; } = dir2;
public string DirPath = "/{Dir1}/{Dir2}";
public string FileId { get; init; } = fileId;
public List<IVirtualFile> Files { get; init; } = files;
public List<IVirtualFile> Files { get; init; } = WithoutDuplicateAnswers(files);
public bool LoadedRemotely { get; set; } = remote;
public bool ScoresUpdated { get; set; }
public ConcurrentDictionary<string, string> FileContents { get; } = [];
public QuestionAndAnswers? Question { get; set; }
public Meta? Meta { get; set; }

public static Meta DeserializeMeta(string json)
{
if (string.IsNullOrEmpty(json))
return new Meta();

var meta = json.FromJson<Meta>();
var toRemove = new List<string>();
foreach (var item in meta.Comments)
{
if (item.Key.Contains('[') || item.Key.Contains(']'))
{
toRemove.Add(item.Key);
}
}
toRemove.ForEach(key => meta.Comments.Remove(key));
return meta;
}

public IVirtualFile? GetMetaFile() => Files.FirstOrDefault(x => x.Name == $"{FileId}.meta.json");

public static List<IVirtualFile> WithoutDuplicateAnswers(List<IVirtualFile> files)
{
var accepted = files.FirstOrDefault(x => x.Name.Contains(".h.accepted"));
var mostVoted = files.FirstOrDefault(x => x.Name.Contains(".h.most-voted"));
return accepted?.Length == mostVoted?.Length
? files.Where(x => !Equals(x, mostVoted)).ToList()
: files;
}

public IEnumerable<IVirtualFile> GetAnswerFiles() => Files.Where(x => x.Name.Contains(".a.") || x.Name.Contains(".h."));

public int GetAnswerFilesCount() => GetAnswerFiles().Count();

public async Task<QuestionAndAnswers?> GetQuestionAsync()
{
Expand All @@ -47,24 +82,19 @@ public class QuestionFiles(int id, string dir1, string dir2, string fileId, List
return Question;
}

public void UpdateScores(List<StatTotals> postStats)
public void ApplyScores(List<StatTotals> postStats)
{
if (Question == null)
throw new ArgumentNullException(nameof(Question));
if (ScoresUpdated)
return;

ScoresUpdated = true;

var map = postStats.ToDictionary(x => x.Id);
foreach (var answer in Question.Answers)

Question.Answers.Sort((a, b) =>
{
if (map.TryGetValue(answer.Id, out var stat))
{
answer.UpVotes = stat.UpVotes;
answer.DownVotes = stat.DownVotes;
}
}
var aScore = postStats.FirstOrDefault(x => x.Id == a.Id)?.GetScore()
?? GetModelScore(a.Model);
var bScore = postStats.FirstOrDefault(x => x.Id == b.Id)?.GetScore()
?? GetModelScore(b.Model);
return bScore - aScore;
});
}

public async Task LoadContentsAsync()
Expand All @@ -77,11 +107,15 @@ public async Task LoadContentsAsync()
await Task.WhenAll(tasks);
}

public string GetAnswerUserName(string answerFileName) => answerFileName[(FileId + ".a.").Length..].LeftPart('.');

public string GetAnswerId(string answerFileName) => Id + "-" + GetAnswerUserName(answerFileName);

public async Task LoadQuestionAndAnswersAsync()
{
var questionFileName = FileId + ".json";
await LoadContentsAsync();

var to = new QuestionAndAnswers();
foreach (var entry in FileContents)
{
Expand All @@ -92,14 +126,14 @@ public async Task LoadQuestionAndAnswersAsync()
}
else if (fileName == $"{FileId}.meta.json")
{
Meta = entry.Value.FromJson<Meta>();
Meta.StatTotals ??= new();
Meta.ModelVotes ??= new();
to.Meta = DeserializeMeta(entry.Value);
to.Meta.StatTotals ??= new();
to.Meta.ModelVotes ??= new();
}
else if (fileName.StartsWith(FileId + ".a."))
{
var answer = entry.Value.FromJson<Answer>();
answer.Id = $"{Id}-{answer.Model.Replace(':','-')}";
answer.Id = GetAnswerId(fileName);
to.Answers.Add(answer);
}
else if (fileName.StartsWith(FileId + ".h."))
Expand All @@ -111,7 +145,6 @@ public async Task LoadQuestionAndAnswersAsync()
Id = $"{Id}-{userName}",
Model = userName,
Created = (post.LastEditDate ?? post.CreationDate).ToUnixTime(),
UpVotes = userName == "most-voted" ? MostVotedScore : AcceptedScore,
Choices = [
new()
{
Expand All @@ -120,22 +153,18 @@ public async Task LoadQuestionAndAnswersAsync()
}
]
};
if (to.Answers.All(x => x.Id != answer.Id))
to.Answers.Add(answer);
to.Answers.Add(answer);
}
}

if (to.Post == null)
return;

Question = to;

to.Answers.Each(x => x.UpVotes = x.UpVotes == 0 ? ModelScores.GetValueOrDefault(x.Model, 1) : x.UpVotes);
to.Answers.Sort((a, b) => b.Votes - a.Votes);

if (Meta?.StatTotals.Count > 0)
if (to.Meta?.StatTotals.Count > 0)
{
UpdateScores(Meta.StatTotals);
ApplyScores(to.Meta.StatTotals);
}

Question = to;
}
}
23 changes: 23 additions & 0 deletions MyApp.ServiceInterface/Data/QuestionsProvider.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
using Microsoft.Extensions.Logging;
using MyApp.ServiceModel;
using ServiceStack;
using ServiceStack.IO;
using ServiceStack.Messaging;
using ServiceStack.Text;

namespace MyApp.Data;

public class QuestionsProvider(ILogger<QuestionsProvider> log, IMessageProducer mqClient, IVirtualFiles fs, R2VirtualFiles r2)
{
public System.Text.Json.JsonSerializerOptions SystemJsonOptions = new(TextConfig.SystemJsonOptions)
{
WriteIndented = true
};

public QuestionFiles GetLocalQuestionFiles(int id)
{
var (dir1, dir2, fileId) = id.ToFileParts();
Expand All @@ -31,6 +38,22 @@ public async Task<QuestionFiles> GetRemoteQuestionFilesAsync(int id)
return new QuestionFiles(id: id, dir1: dir1, dir2: dir2, fileId: fileId, files: files, remote:true);
}

public static string GetMetaPath(int id)
{
var (dir1, dir2, fileId) = id.ToFileParts();
var metaPath = $"{dir1}/{dir2}/{fileId}.meta.json";
return metaPath;
}

public async Task WriteMetaAsync(Meta meta)
{
var metaJson = System.Text.Json.JsonSerializer.Serialize(meta, SystemJsonOptions);
var metaPath = GetMetaPath(meta.Id);
await Task.WhenAll(
fs.WriteFileAsync(metaPath, metaJson),
r2.WriteFileAsync(metaPath, metaJson));
}

public async Task<QuestionFiles> GetQuestionFilesAsync(int id)
{
var localFiles = GetLocalQuestionFiles(id);
Expand Down
7 changes: 6 additions & 1 deletion MyApp.ServiceInterface/Data/StatUtils.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using MyApp.ServiceModel;

namespace MyApp.Data;

public static class StatUtils
{
public static string? GetUserName(this HttpContext? ctx) => ctx?.User.GetUserName();
public static string? GetUserName(this ClaimsPrincipal? user) => user?.Identity?.Name;
public static bool IsAdminOrModerator(this ClaimsPrincipal? user) => Stats.IsAdminOrModerator(user?.Identity?.Name);

public static T WithRequest<T>(this T stat, HttpContext? ctx) where T : StatBase
{
var user = ctx?.User;
Expand Down
32 changes: 32 additions & 0 deletions MyApp.ServiceInterface/UserServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,36 @@ public async Task<object> Any(GetUserAvatar request)
}
return new HttpResult(Svg.GetImage(Svg.Icons.Users), MimeTypes.ImageSvg);
}

public async Task<object> Any(UserPostData request)
{
var userName = Request.GetClaimsPrincipal().Identity!.Name!;
var allUserPostVotes = await Db.SelectAsync<Vote>(x => x.PostId == request.PostId && x.UserName == userName);

var to = new UserPostDataResponse
{
UpVoteIds = allUserPostVotes.Where(x => x.Score > 0).Select(x => x.RefId).ToSet(),
DownVoteIds = allUserPostVotes.Where(x => x.Score < 0).Select(x => x.RefId).ToSet(),
};
return to;
}

public async Task Any(PostVote request)
{
var userName = Request.GetClaimsPrincipal().Identity!.Name!;
if (string.IsNullOrEmpty(userName))
throw new ArgumentNullException(nameof(userName));
var postId = request.RefId.LeftPart('-').ToInt();
var score = request.Up == true ? 1 : request.Down == true ? -1 : 0;
MessageProducer.Publish(new DbWriteTasks
{
RecordPostVote = new()
{
RefId = request.RefId,
PostId = postId,
UserName = userName,
Score = score,
}
});
}
}
17 changes: 14 additions & 3 deletions MyApp.ServiceModel/Meta.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
namespace MyApp.ServiceModel;

// AnswerId = `{PostId}-{UserName}`
// RefId = PostId | AnswerId
public class Meta
{
// PostId
public int Id { get; set; }

// ModelName => Votes
public Dictionary<string, int> ModelVotes { get; set; } = [];

// RefId => Comments
public Dictionary<string, List<Comment>> Comments { get; set; } = [];

// Question + Answer Stats Totals
public List<StatTotals> StatTotals { get; set; }
public Dictionary<string, int> ModelVotes { get; set; }
public List<Comment> Comments { get; set; } = [];
public List<StatTotals> StatTotals { get; set; } = [];

public DateTime ModifiedDate { get; set; }
}
Loading

0 comments on commit 71cc4a5

Please sign in to comment.