https://github.com/lion-200/CurationAssistant
http://www.curationassistant.com
I recently started with curation activities on Steem by submitting good posts I read to curator groups like @c-squared and
@curie.
Now that I have started the curation training for @curie from
@gibic, I see that the life of a curator is not easy at all.
There are a lot of items in the checklist a curator needs to check before a post can be submitted for a nice upvote. This is something all curator groups have in common.
Curator groups with a higher upvote even have a more strict criteria before upvoting a submitted post.
Curation Assistant is an "assistant" for the curator to help him/her out by checking some basic rules defined by the curator.
The intention of Curation Assistant is to reduce the time spent on checking administrative facts, so that the curator can have more time to curate good stuff, which is good for the whole community. This project is set up generic, so that it can be used by more curator groups like @curie,
@ocd,
@c-squared etc.
An example of a rule is the post creation date range for a submitted post to be eligable for an upvote from curie.
Posts must be more than 30 minutes old, but less than 72 hours old, with maximum $2 pending payout.
There are more rules of this kind in other curation groups too.
Curation Assistant is developed to reduce time to check the technical/administrative details, so that curators can focus more on the content of the post.
There are still a lot of other manual checks needed to validate a post of course, like the check for plagiarism etc.
At least I made a start to build a tool which might be useful and hopefully helpful for curators of all groups.
I am a C# .NET developer, that's why I developed this tool with C# .NET.
There are not many projects on Steem developed with this programming language. I hope it grows in time, and I hope this tool can help others to build something of their own in .NET.
I created this tool with Visual Studio 2019 Community Edition.
The projects are targeting .NET Framework 4.7.2.
The following NuGet packages are used in this project frontend and backend:
For the calls I make to Steem API, I used the Steem.NET project created by @arcange: https://github.com/VIM-Arcange/Steem.NET
I am contributing to his project by sending PR's for new methods I use in Curation Assistant. Thanks for your contribution @arcange, and of course your support!
I added Steem.NET as a separate class library to the solution to easily keep track of the changes and keep the functionalities separated.
For parsing the responses received from Steem API, I use Newtonsoft.Json to map the results to classes.
For displaying the data in a proper way, I use KnockoutJS which I also used for a different project before at my work. I like the simplicity of KnockoutJS.
And for some basic logging I use log4net.
Currently there are 3 sections on the page which needs some explanation:
In this section, the user of the tool can decide on the Validation variables/rules which will be run on the post he/she wants to validate.
Currently the following rules are implemented:
There is not much necessary to explain here then to put the link of the post here to validate & show details in the section below.
In this section there are 3 blocks which can be useful to check:
After putting the post link and hitting enter or "validate post" link, this is the result:
Validation summary
Author details
Author transaction history
Post details
For some statistics I needed to convert some solutions for calculating SP, Rep, VP etc. to C# code.
I created a ViewModel which contains the response of the Steem Api method mapped to a class, enriched with calculation data.
Below is an example of the response processing I implemented for get_accounts method and the enrichment with calculations:
For better readabilty I tried syntax highlighting but couldn't get it to work for C# code, so please bear with me for standard code syntax.
private AuthorViewModel GetAuthor(string accountName)
{
var author = new AuthorViewModel();
try
{
var globalProperties = GetDynamicGlobalProperties();
var steemPerMVests = CalculationHelper.CalculateSteemPerMVests(globalProperties);
author.Details = GetAccountDetails(accountName);
author.ReputationCalculated = CalculationHelper.CalculateReputation(author.Details.reputation);
author.SteemPowerCalculated = CalculationHelper.CalculateSteemPower(author.Details.vesting_shares, steemPerMVests);
author.VotingManaPercentageCalculated = CalculationHelper.CalculateVotingManaPercentage(author.Details);
}
catch (Exception ex)
{
log.Error(ex);
}
return author;
}
This is the actual call to get response from Steem API:
private GetAccountsModel GetAccountDetails(string accountName)
{
var account = new GetAccountsModel();
try
{
if (!String.IsNullOrEmpty(accountName))
{
using (var csteemd = new CSteemd(ConfigurationHelper.HostName))
{
// account details
var response = csteemd.get_accounts(new ArrayList() { accountName });
if (response != null)
{
var accountResult = response.ToObject<List<GetAccountsModel>>();
account = accountResult.FirstOrDefault();
}
}
}
}
catch (Exception ex)
{
log.Error(ex);
}
return account;
}
Here are the C# variants of calculating SP, Reputation and VP %:
Calculate reputation
public static decimal CalculateReputation(string repString)
{
decimal repResult = 0;
double rep = 0;
double.TryParse(repString, out rep);
rep = Math.Log10(rep);
rep -= 9;
rep *= 9;
rep += 25;
if (rep > 0)
{
repResult = (Decimal)Math.Round(rep, 2);
}
return repResult;
}
Calculate steem power
public static decimal CalculateSteemPower(string spString, decimal steemPerMVests)
{
var veryLargeNumber = 1e6;//Equals to 1*10^6
double sp = 0;
if (!String.IsNullOrEmpty(spString))
{
spString = spString.Replace("VESTS", "").Trim();
double.TryParse(spString, out sp);
sp = sp / veryLargeNumber * (double)steemPerMVests;
}
return Math.Round((decimal)sp, 2);
}
Calculate VP %
public static decimal CalculateVotingManaPercentage(GetAccountsModel accountDetails)
{
var secondsAgo = DateTime.UtcNow.Subtract(accountDetails.last_vote_time).TotalSeconds;
var vpNow = accountDetails.voting_power + (10000 * secondsAgo / 432000);
var vp = Math.Min(Math.Round(vpNow / 100, 2), 100);
return (decimal)vp;
}
Validation
I chose not to be very generic regarding the validation rules. That might change in the future but for now, these are the predefined rules and how I implemented them.
Basically I have a list of variables provided by user input, and pass these to the ValidationHelper methods to run specific validations:
public ValidationSummaryViewModel RunValidation(CurationDetailsViewModel model, ValidationVariables vars)
{
var result = new ValidationSummaryViewModel();
var validationItems = new List<ValidationItemViewModel>();
try
{
validationItems.Add(ValidationHelper.ValidatePostCreateDateRule(model, vars));
validationItems.Add(ValidationHelper.ValidatePostMaxPendingPayoutRule(model, vars));
validationItems.Add(ValidationHelper.ValidateAuthorReputationRule(model, vars));
validationItems.Add(ValidationHelper.ValidateMinPostsRule(model, vars));
validationItems.Add(ValidationHelper.ValidateMinCommentsRule(model, vars));
var upvoteAccountDetails = GetAccountDetails(vars.UpvoteAccount.Replace("@", ""));
validationItems.Add(ValidationHelper.ValidateUpvoteAccountMinVPRule(upvoteAccountDetails, vars));
}
catch (Exception ex)
{
log.Error(ex);
}
result.Items = validationItems;
return result;
}
Below you can see the actual implementation of Post Create Date Rule:
public static ValidationItemViewModel ValidatePostCreateDateRule(CurationDetailsViewModel model, ValidationVariables vars)
{
ValidationPriority prio = ValidationPriority.High;
ValidationResultType resultType = ValidationResultType.Failure;
var validationItem = new ValidationItemViewModel();
validationItem.Title = string.Format("Post creation date is > {0} minutes and < {1} hours", vars.PostCreatedAtMin, vars.PostCreatedAtMax / 60);
validationItem.Priority = prio;
validationItem.PriorityDescription = prio.ToString();
validationItem.OrderId = 1;
var postCreatedDate = model.BlogPost.Details.created;
// Check if the post creation date is between the required ranges
if (DateTime.Now > postCreatedDate.AddMinutes(vars.PostCreatedAtMin) &&
DateTime.Now < postCreatedDate.AddMinutes(vars.PostCreatedAtMax))
{
resultType = ValidationResultType.Success;
}
else
{
resultType = ValidationResultType.Failure;
}
validationItem.ResultType = resultType;
validationItem.ResultTypeDescription = resultType.ToString();
var span = (TimeSpan)(DateTime.Now - postCreatedDate);
validationItem.ResultMessage = string.Format("Post created {0} days, {1} hours, {2} minutes ago.", span.Days, span.Hours, span.Minutes);
return validationItem;
}
Each Validation method returns a ValidationItemViewModel containing positive or negative result along with the rule.
And in the end, the result is posted to Frontend as Json:
As I mentioned, I used KnockoutJS to bind the Backend classes to Frontend objects.
As an example, this is how the Author details block looks like in KnockoutJs and HTML.
First we make sure the response we got from Backend to be mapped to our object:
We can map all child properties of the returned result automatically by using Knockout.Mapping library.
If you don't plan to do any fancy stuff, you can leave out the mapping and print out the values already. But because I am combining some values and present them differently than how they are received, I mapped them to observables.
function CurationDetailsViewModel(data) {
var self = this;
self.Author = ko.observable();
self.BlogPost = ko.observable();
self.ValidationSummary = ko.observable();
self.BaseUrl = ko.observable("https://steemit.com");
var mapping = {
'Author': {
create: function (options) {
return new AuthorViewModel(options.data, self);
}
},
'BlogPost': {
create: function (options) {
return new BlogPostViewModel(options.data, self);
}
},
'Comments': {
create: function (options) {
return new ActionViewModel(options.data, self);
}
},
'Posts': {
create: function (options) {
return new ActionViewModel(options.data, self);
}
},
'Votes': {
create: function (options) {
return new ActionViewModel(options.data, self);
}
},
'ValidationSummary': {
create: function (options) {
return new ValidationSummaryViewModel(options.data, self);
}
}
}
if (data != null)
ko.mapping.fromJS(data, mapping, self);
}
The html is pretty clean:
<tr>
<th scope="row">Account</th>
<td>
<a data-bind="attr: { href: $root.BaseUrl() + '/@@' + Author().Details.name() }" target="_blank">
<span data-bind="text: Author().Details.name"></span>
</a>
</td>
</tr>
<tr>
<th scope="row">SteemPower</th>
<td>
<span class="btn btn-primary" data-bind="text: Author().SteemPowerCalculated()"></span>
</td>
</tr>
For other KnockoutJS examples and Backend code, please feel free to check out the code in my GitHub repository.
I think there is now a good foundation to build more rules on.
Although there is not a strict roadmap, I think the following items can be very useful to implement:
This list can grow with some feedback from Curation Groups and individual curators.
For Backend as well as Frontend you are welcome to contribute to this project.
I am a C# .NET backend developer for more than a decade. Although I do some frontend work too, my skills and interests lie more on the backend.
The project I have worked on and currently I am working on are Web projects for mostly internal use for the companies I work(ed) for. That's why putting a lot of things I did on Open Source was not an option.
I will post more of my work (which can be shared) via my GitHub repo in the future.
Steem account obviously: @lion200
Discord account: @Lion_200
E-mail: [email protected] or [email protected]