From 447923b1f16911d7dd0b7ee5acc9586d7b233d42 Mon Sep 17 00:00:00 2001 From: codl Date: Mon, 7 May 2018 23:50:37 +0200 Subject: [PATCH] add mechanism for keeeping track of a user's instances --- libforget/known_instances.py | 84 ++++++++++++++++++++++++++++++++++++ libforget/mastodon.py | 6 +-- routes/__init__.py | 9 +++- test/test_known_instances.py | 59 +++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 libforget/known_instances.py create mode 100644 test/test_known_instances.py diff --git a/libforget/known_instances.py b/libforget/known_instances.py new file mode 100644 index 0000000..5f4f214 --- /dev/null +++ b/libforget/known_instances.py @@ -0,0 +1,84 @@ +import json + +def find(predicate, iterator, default=None): + """ + returns the first element of iterator that matches predicate + or default if none is found + """ + try: + return next((el for el in iterator if predicate(el))) + except StopIteration: + return default + +class KnownInstances(object): + def __init__(self, serialised=None, top_slots=3): + self.instances = list() + self.top_slots = top_slots + try: + unserialised = json.loads(serialised) + if not isinstance(unserialised, list): + return self.__default() + for instance in unserialised: + if 'instance' in instance and 'hits' in instance: + self.instances.append(dict( + instance=instance['instance'], + hits=instance['hits'] + )) + except (json.JSONDecodeError, TypeError): + return self.__default() + + def __default(self): + self.instances = [{ + "instance": "mastodon.social", + "hits": 5 + }] + + def clear(self): + self.instances = [] + + def bump(self, instance_name, bump_by=1): + instance = find( + lambda i: i['instance'] == instance_name, + self.instances) + if not instance: + instance = dict(instance=instance_name, hits=0) + self.instances.append(instance) + instance['hits'] += bump_by + + def normalize(self): + """ + raises the top `top_slots` instances to the top, + making sure not to move instances that were already at + the top + """ + top_slots = self.top_slots + head = self.instances[:top_slots] + tail = self.instances[top_slots:] + if len(tail) == 0: + return + + def key(instance): + return instance['hits'] + + for _ in range(top_slots): + head_min = min(head, key=key) + tail_max = max(tail, key=key) + if tail_max['hits'] > head_min['hits']: + # swap them + i = head.index(head_min) + j = tail.index(tail_max) + buf = head[i] + head[i] = tail[j] + tail[j] = buf + + else: + break + + self.instances = head + tail + + def top(self): + head = self.instances[:self.top_slots] + return tuple((i['instance'] for i in head)) + + def serialize(self): + return json.dumps(self.instances) diff --git a/libforget/mastodon.py b/libforget/mastodon.py index 8e01da1..68a2a1a 100644 --- a/libforget/mastodon.py +++ b/libforget/mastodon.py @@ -210,11 +210,11 @@ def delete(post): raise TemporaryError(e) -def suggested_instances(limit=5, min_popularity=5): +def suggested_instances(limit=5, min_popularity=5, blacklist=tuple()): return ( MastodonInstance.query .filter(MastodonInstance.popularity > min_popularity) - .order_by(db.desc(MastodonInstance.instance == 'mastodon.social'), - db.desc(MastodonInstance.popularity), + .filter(~MastodonInstance.instance.in_(blacklist)) + .order_by(db.desc(MastodonInstance.popularity), MastodonInstance.instance) .limit(limit).all()) diff --git a/routes/__init__.py b/routes/__init__.py index 9f31937..83c79d4 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -6,6 +6,7 @@ import libforget.mastodon from libforget.auth import require_auth, csrf,\ get_viewer from model import Session, TwitterArchive, MastodonApp +from libforget.known_instances import KnownInstances from app import app, db, sentry, imgproxy import tasks from zipfile import BadZipFile @@ -264,7 +265,13 @@ def mastodon_login_step2(instance_url): db.session.commit() g.viewer = session - return redirect(url_for('index')) + + ki = KnownInstances(request.cookies.get('forget_known_instances', '')) + ki.bump(instance_url) + + resp = redirect(url_for('index')) + resp.set_cookie('forget_known_instances', ki.serialize()) + return resp @app.route('/sentry/setup.js') diff --git a/test/test_known_instances.py b/test/test_known_instances.py new file mode 100644 index 0000000..1b406c7 --- /dev/null +++ b/test/test_known_instances.py @@ -0,0 +1,59 @@ +from libforget.known_instances import KnownInstances + + +def test_known_instances_defaults(): + ki = KnownInstances() + assert len(ki.instances) == 1 + assert ki.instances[0]['instance'] == 'mastodon.social' + assert ki.instances[0]['hits'] > 0 + + +def test_known_instances_clear(): + ki = KnownInstances() + ki.clear() + assert len(ki.instances) == 0 + + +def test_known_instances_deserialize(): + ki = KnownInstances(""" [ + {"instance": "chitter.xyz", "hits": 666, "foo": "bar"}, + {"instance": "invalid"} + ] """) + assert len(ki.instances) == 1 + assert ki.instances[0]['instance'] == "chitter.xyz" + assert ki.instances[0]['hits'] == 666 + + +def test_known_instances_bump(): + ki = KnownInstances() + ki.bump('chitter.xyz') + assert len(ki.instances) == 2 + assert ki.instances[1]['instance'] == "chitter.xyz" + assert ki.instances[1]['hits'] == 1 + + ki.bump('chitter.xyz') + assert len(ki.instances) == 2 + assert ki.instances[1]['instance'] == "chitter.xyz" + assert ki.instances[1]['hits'] == 2 + + +def test_known_instances_normalize_top(): + ki = KnownInstances() + ki.clear() + ki.normalize() + assert len(ki.instances) == 0 + + ki.bump("a", 1) + ki.bump("b", 2) + ki.bump("c", 3) + ki.normalize() + assert ki.instances[0]['instance'] == "a" + assert ki.instances[1]['instance'] == "b" + assert ki.instances[2]['instance'] == "c" + + ki.bump("d", 4) + ki.normalize() + assert ki.instances[0]['instance'] == "d" + assert ki.instances[3]['instance'] == "a" + + assert ki.top() == ("d", "b", "c")