diff --git a/doc/ratings-and-trust.md b/doc/ratings-and-trust.md new file mode 100644 index 0000000..a17a309 --- /dev/null +++ b/doc/ratings-and-trust.md @@ -0,0 +1,17 @@ +# Ratings and trust + +Rein has two systems in place to help users decide with whom to work with and who to avoid. + +## Ratings + +The first and more simple of the two systems is a five-star rating system. After a job is completed, either through acceptance of the delivery by the client or through successful mediation, all involved users (client, mediator and worker) are given the option to rate the other two participants on a scale from 0 to 5. If a user so desires, he can also leave a comment regarding the other person's performance. + +Across the application, these simple five star ratings are used as an indicator of performance by averaging all ratings a user has received and providing the total number of ratings for reference. A user's ratings, comments on his performance and who he has been rated by can be viewed in more detail on their ratings page. + +## Trust score + +A slightly more sophisticated system allows the user to calculate trust scores for other users on their ratings page. This can be done either automatically, if the user has enabled the setting on his settings page, or manually by clicking a button on an individual ratings page. + +The trust score system is based on the idea that you usually don't trust someone you have no connection to. How would you know that their ratings aren't fake? Usually, you seek out clients, workers or mediators based on someone you do know vouching for them. + +If you're interested in the details, check out https://wiki.bitcoin-otc.com/wiki/OTC_Rating_System#Notes_about_gettrust as Rein's implementation is based on that very idea. \ No newline at end of file diff --git a/rein/cli.py b/rein/cli.py index c37c970..0411411 100644 --- a/rein/cli.py +++ b/rein/cli.py @@ -28,7 +28,7 @@ from .lib.script import build_2_of_3, build_mandatory_multisig, check_redeem_scripts from .lib.localization import init_localization from .lib.transaction import partial_spend_p2sh, spend_p2sh, spend_p2sh_mediator, partial_spend_p2sh_mediator, partial_spend_p2sh_mediator_2 -from .lib.rating import add_rating, get_user_jobs, get_average_user_rating, get_average_user_rating_display, get_all_user_ratings +from .lib.rating import add_rating, get_user_jobs, get_average_user_rating, get_average_user_rating_display, get_all_user_ratings, calculate_trust_score # Import config import rein.lib.config as config @@ -1202,16 +1202,12 @@ def status(multi, identity, jobid): else: click.echo("Job id not found") - -@cli.command() -@click.argument('key', required=True) -@click.argument('value', required=True) -def config(key, value): +def config_common(key, value): """ Set configuration variable. Parses true/false, on/off, and passes anything else unaltered to the db. """ - keys = ['testnet', 'tor', 'debug', 'fee'] + keys = ['testnet', 'tor', 'debug', 'fee', 'trust_score'] if key not in keys: click.echo("Invalid config setting. Try one of " + ', '.join(keys)) return @@ -1223,6 +1219,17 @@ def config(key, value): else: PersistConfig.set(rein, key, value) +@cli.command() +@click.argument('key', required=True) +@click.argument('value', required=True) +def config(key, value): + """ + Set configuration variable. Parses true/false, on/off, and passes + anything else unaltered to the db. + """ + + config_common(key, value) + # leave specific config commands in for backwards compatibility, remove in 0.4 @cli.command() @@ -1537,8 +1544,9 @@ def rate_web(): @app.route('/ratings/', methods=['GET']) def view_ratings(msin): + display_trust_score = PersistConfig.get(rein, 'trust_score', False) ratings = get_all_user_ratings(log, url, user, rein, msin) - return render_template("ratings.html", user=user, user_rated=get_user_name(log, url, user, rein, msin), msin=msin, ratings=ratings) + return render_template("ratings.html", user=user, user_rated=get_user_name(log, url, user, rein, msin), msin=msin, ratings=ratings, display_trust_score=display_trust_score) @app.route('/hide', methods=['POST']) def hide(): @@ -1576,6 +1584,25 @@ def unhide(): except: return 'false' + @app.route('/config', methods=['POST']) + def config_web(): + """Allows for changes to the user's config via the web interface""" + + try: + key = request.json['key'] + value = request.json['value'] + config_common(key, value) + + except: + return 'false' + + return 'true' + + @app.route('/trust_score/', defaults={'source_msin': user.msin}) + @app.route('/trust_score//', methods=['GET']) + def trust_score(dest_msin, source_msin): + return calculate_trust_score(dest_msin, source_msin, rein) + @app.route('/settings', methods=['GET']) def settings(): """Allows for local customization of the web app.""" @@ -1592,7 +1619,10 @@ def settings(): for hidden_mediator in hidden_mediators: hidden_mediator['unhide_button'] = HiddenContent.unhide_button('mediator', hidden_mediator['content_identifier']) - return render_template('settings.html', user=user, hidden_jobs=hidden_jobs, hidden_bids=hidden_bids, hidden_mediators=hidden_mediators) + fee = float(PersistConfig.get(rein, 'fee', 0.001)) + trust_score = PersistConfig.get(rein, 'trust_score', False) + + return render_template('settings.html', user=user, hidden_jobs=hidden_jobs, hidden_bids=hidden_bids, hidden_mediators=hidden_mediators, fee=fee, trust_score=trust_score) @app.route("/post", methods=['POST', 'GET']) def job_post(): diff --git a/rein/html/css/style_v2.css b/rein/html/css/style_v2.css index 1709ef2..5e6f42c 100644 --- a/rein/html/css/style_v2.css +++ b/rein/html/css/style_v2.css @@ -2154,6 +2154,9 @@ border-radius: 50%;} } .thumbnail {margin-bottom:0;} .control-group {margin-bottom:30px;} +hr { + border-top: 1px solid #000; +} @media (min-width: 768px) { #sidebar-left.col-sm-2 { opacity: 1; diff --git a/rein/html/js/rate.js b/rein/html/js/rate.js index 9b63def..7660e35 100644 --- a/rein/html/js/rate.js +++ b/rein/html/js/rate.js @@ -72,6 +72,7 @@ function nextJob() { document.addEventListener("DOMContentLoaded", function(event) { // Initialize contents of the job and user id fields and their labels $(".ratingdiv").raty(); + $(".ratingdiv").raty({score: 1}); $(".ratingdiv").click(function() { $("input[name='rating']").val($(".ratingdiv").raty('score')); }); diff --git a/rein/html/js/settings.js b/rein/html/js/settings.js new file mode 100644 index 0000000..4a8903e --- /dev/null +++ b/rein/html/js/settings.js @@ -0,0 +1,39 @@ +function postError(data) { + if (data != 'true') { + alert('Your desired setting could not be saved.') + } else { + alert('Setting saved successfully!') + } +} + +function setFee() { + fee = $('#feeInput').val(); + $.ajax({ + method: "POST", + url: "/config", + contentType: "application/json", + data: JSON.stringify({ + 'key': 'fee', + 'value': fee + }), + success: function(data) { + postError(data); + } + }) +} + +function setTrustScore() { + trustScoreEnabled = $('#trustScore').is(':checked'); + $.ajax({ + method: "POST", + url: "/config", + contentType: "application/json", + data: JSON.stringify({ + 'key': 'trust_score', + 'value': trustScoreEnabled.toString() + }), + success: function(data) { + postError(data); + } + }) +} \ No newline at end of file diff --git a/rein/html/js/trustScore.js b/rein/html/js/trustScore.js new file mode 100644 index 0000000..189c2e5 --- /dev/null +++ b/rein/html/js/trustScore.js @@ -0,0 +1,22 @@ +function setTrustScore(msin, displayId) { + trustScore = getTrustScore(msin); + $('#' + displayId).html(trustScore); +} + +function getTrustScore(msin) { + result = 'Trust score could not be calculated'; + $.ajax({ + method: "GET", + url: "/trust_score/" + msin, + contentType: "application/json", + async: false, + success: function(data) { + data = JSON.parse(data); + result = 'No trust links between you and the user were found. A trust score could not be calculated.' + if (data['links'] != 0) { + result = 'Trust score: ' + data['score'] + ', Trust links: ' + data['links']; + } + } + }) + return result; +} \ No newline at end of file diff --git a/rein/html/ratings.html b/rein/html/ratings.html index 46b968b..fd3807b 100644 --- a/rein/html/ratings.html +++ b/rein/html/ratings.html @@ -2,9 +2,23 @@ {% from "_form_helpers.html" import render_error %} {% block body %} + +
- This page displays ratings received by {{ user_rated }} (msin: {{ msin }}). +

This page displays ratings received by {{ user_rated }} (msin: {{ msin }}).

+

+ + {% if display_trust_score %} + + {% else %} +

+ {% endif %} +

diff --git a/rein/html/settings.html b/rein/html/settings.html index c2027b9..b106199 100644 --- a/rein/html/settings.html +++ b/rein/html/settings.html @@ -2,14 +2,18 @@ {% from "_form_helpers.html" import render_error %} {% block body %} + +

Settings

-

Hidden content

+
+ +

Hidden content

This section lets you view and unhide hidden content.

-
Jobs
+

Jobs

{% if hidden_jobs %}
@@ -31,7 +35,7 @@
Jobs

No jobs have been hidden.

{% endif %} -
Bids
+

Bids

{% if hidden_bids %}
@@ -53,7 +57,7 @@
Bids

No bids have been hidden.

{% endif %} -
Mediators
+

Mediators

{% if hidden_mediators %}
@@ -74,6 +78,44 @@
Mediators
{% else %}

No mediators have been hidden.

{% endif %} + +
+ +

Transaction fee

+

This section lets you adjust the fee that is used for transactions from escrows to mediators and workers.

+ + +
+ + +
+
+ +
+ + +
+ +

Trust score

+

Would you like to automatically display trust scores for users on their ratings page? If so, check the box below. This may make ratings pages slower to load.

+ + +
+ + {% if trust_score %} + + {% endif %} + +
+
+ +
+ + {% endblock %} diff --git a/rein/lib/forms.py b/rein/lib/forms.py index e104525..eab92ac 100644 --- a/rein/lib/forms.py +++ b/rein/lib/forms.py @@ -88,5 +88,5 @@ class RatingForm(Form): job_id = TextField(_('Select job'), validators = [Required()], default='') user_id = TextField(_('Select user'), validators = [Required(), avoid_self_rating], default='') rated_by_id = TextField(_('Your SIN'), validators = [Required()], default='') - rating = TextField(_('Rating'), validators=[Required()], default=0, widget=HiddenInput()) + rating = TextField(_('Rating'), validators=[Required()], default=1, widget=HiddenInput()) comments = TextAreaField(_('Comments'), validators = [], default='') diff --git a/rein/lib/rating.py b/rein/lib/rating.py index bbd783d..d39dd3b 100644 --- a/rein/lib/rating.py +++ b/rein/lib/rating.py @@ -178,3 +178,75 @@ def get_all_user_ratings(log, url, user, rein, msin): ) return ratings + +def calculate_trust_score(dest_msin=None, source_msin=None, rein=None, test=False, test_ratings=[]): + """Calculates the trust score for a user as identified by his msin. + Algorithm based on the level 2 trust system implemented by Bitcoin OTC + and outlined at https://wiki.bitcoin-otc.com/wiki/OTC_Rating_System#Notes_about_gettrust.""" + + # Grab all ratings commited by source_msin (the client) + ratings_by_source = None + if not test: + ratings_by_source = rein.session.query(Document).filter(and_( + Document.doc_type == 'rating', + Document.contents.like('%\nRater msin: {}%'.format(source_msin)) + )).all() + + else: + ratings_by_source = [test_rating for test_rating in test_ratings if test_rating['Rater msin'] == 'SourceMsin'] + + # Compile list of users (by msin) that have been rated by source and their ratings + rated_by_source = [] + for rating in ratings_by_source: + rating_dict = None + if not test: + rating_dict = document_to_dict(rating.contents) + + else: + rating_dict = rating + + msin_rated = rating_dict['User msin'] + rating_value = int(rating_dict['Rating']) + if not msin_rated in rated_by_source: + rated_by_source.append((msin_rated, rating_value)) + + # Determine if any of the users rated by source or source have rated dest + # Add source to the list of users source has vouched for with full trust + vouched_users = rated_by_source + [(source_msin, 5)] + # Calculate trust links between source and dest + trust_links = [] + for vouched_user in vouched_users: + (vouched_user_msin, vouched_user_trust) = vouched_user + dest_ratings_by_vouched_user = None + if not test: + dest_ratings_by_vouched_user = rein.session.query(Document).filter( + and_( + Document.doc_type == 'rating', + Document.contents.like('%\nRater msin: {}%'.format(vouched_user_msin)), + Document.contents.like('%\nUser msin: {}%'.format(dest_msin)) + )).all() + else: + dest_ratings_by_vouched_user = [test_rating for test_rating in test_ratings if test_rating['Rater msin'] == vouched_user_msin and test_rating['User msin'] == 'DestMsin'] + + for rating in dest_ratings_by_vouched_user: + link_rating = None + if not test: + link_rating = document_to_dict(rating.contents) + + else: + link_rating = rating + + trust_values = [ + vouched_user_trust, + int(link_rating['Rating']) + ] + trust_links.append(min(trust_values)) + + dest_trust_score = { + 'score': float(sum(trust_links)) / float(len(trust_links)), + 'links': len(trust_links) + } + if not test: + dest_trust_score = json.dumps(dest_trust_score) + + return dest_trust_score diff --git a/tests/lib/test_trust_score.py b/tests/lib/test_trust_score.py new file mode 100644 index 0000000..29168b9 --- /dev/null +++ b/tests/lib/test_trust_score.py @@ -0,0 +1,67 @@ +import unittest + +from rein.lib.rating import calculate_trust_score + +# Test cases have to follow certain conventions +# List of dicts / lists of dicts +# Each dict represents a rating, containing the keys +# Rater msin, User msin, Rating +# There must be ratings listing SourceMsin as rater +# The people rated by SourceMsin must have rated (or not rated) DestMsin +############################################### +# Test case 1 flow chart +# 5/5 4/5 +#Link One <------------+ Source +-------------> Link Three +# + + + +# | |3/5 | +# | v | +# | Link Two | +# | + | +# | | | +# | |5/5 | +# | | | +# | v | +# | | +# +---------------------> Dest <-------------------------+ +# 5/5 5/5 + +test_case_1 = [ + { + 'Rater msin': 'SourceMsin', + 'User msin': 'LinkOneMsin', + 'Rating': 5 + }, + { + 'Rater msin': 'SourceMsin', + 'User msin': 'LinkTwoMsin', + 'Rating': 3 + }, + { + 'Rater msin': 'SourceMsin', + 'User msin': 'LinkThreeMsin', + 'Rating': 4 + }, + { + 'Rater msin': 'LinkOneMsin', + 'User msin': 'DestMsin', + 'Rating': 5 + }, + { + 'Rater msin': 'LinkTwoMsin', + 'User msin': 'DestMsin', + 'Rating': 5 + }, + { + 'Rater msin': 'LinkThreeMsin', + 'User msin': 'DestMsin', + 'Rating': 5 + } +] + +class TestTrustScore(unittest.TestCase): + def test_trust_score(self): + """Tests trust score calculation""" + + trust_score_1 = calculate_trust_score(test=True, test_ratings=test_case_1) + self.assertEqual(trust_score_1['score'], 4) + self.assertEqual(trust_score_1['links'], 3)