From 05db96236ce2652131efd5b43097253157e3f070 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 9 Nov 2021 09:21:32 +0100 Subject: [PATCH 01/22] add misskey icon --- assets/misskey.png | Bin 0 -> 18767 bytes dodo.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 assets/misskey.png diff --git a/assets/misskey.png b/assets/misskey.png new file mode 100644 index 0000000000000000000000000000000000000000..1e57da6b38197f95f848fcdcb2308e4e490cae8a GIT binary patch literal 18767 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 literal 0 HcmV?d00001 diff --git a/dodo.py b/dodo.py index 75ceb08..f737ba2 100644 --- a/dodo.py +++ b/dodo.py @@ -60,7 +60,7 @@ def task_service_icon(): formats = ('webp', 'png') for width in widths: for image_format in formats: - for basename in ('twitter', 'mastodon'): + for basename in ('twitter', 'mastodon', 'misskey'): yield dict( name='{}-{}.{}'.format(basename, width, image_format), actions=[(resize_image, (basename, width, image_format))], From ce35aa939b7ed21bca7e03aa257a02992b525c7c Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 9 Nov 2021 09:23:22 +0100 Subject: [PATCH 02/22] add misskey to web pages --- assets/instance_buttons.js | 29 +++++++++++------ templates/about.html | 63 ++++++++++++++++++++++++++++++++++-- templates/misskey_login.html | 29 +++++++++++++++++ 3 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 templates/misskey_login.html diff --git a/assets/instance_buttons.js b/assets/instance_buttons.js index 6c2bfc1..0b152cc 100644 --- a/assets/instance_buttons.js +++ b/assets/instance_buttons.js @@ -3,13 +3,22 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances. (function instance_buttons(){ const container = document.querySelector('#mastodon_instance_buttons'); - const button_template = Function('first', 'instance', - 'return `' + document.querySelector('#instance_button_template').innerHTML + '`;'); - const another_button_template = Function( + const mastodon_button_template = Function('first', 'instance', + 'return `' + document.querySelector('#mastodon_instance_button_template').innerHTML + '`;'); + const mastodon_another_button_template = Function( 'return `' + - document.querySelector('#another_instance_button_template').innerHTML + '`;'); - const top_instances = - Function('return JSON.parse(`' + document.querySelector('#top_instances').innerHTML + '`);')(); + document.querySelector('#mastodon_another_instance_button_template').innerHTML + '`;'); + const mastodon_top_instances = + Function('return JSON.parse(`' + document.querySelector('#mastodon_top_instances').innerHTML + '`);')(); + + const misskey_container = document.querySelector('#misskey_instance_buttons'); + const misskey_button_template = Function('first', 'instance', + 'return `' + document.querySelector('#misskey_instance_button_template').innerHTML + '`;'); + const misskey_another_button_template = Function( + 'return `' + + document.querySelector('#misskey_another_instance_button_template').innerHTML + '`;'); + const misskey_top_instances = + Function('return JSON.parse(`' + document.querySelector('#misskey_top_instances').innerHTML + '`);')(); async function get_known(){ let known = known_load(); @@ -39,7 +48,7 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances. known_save(known); let filtered_top_instances = [] - for(let instance of top_instances){ + for(let instance of mastodon_top_instances){ let found = false; for(let k of known){ if(k['instance'] == instance['instance']){ @@ -58,13 +67,13 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances. let first = true; for(let instance of instances){ - html += button_template(first, instance['instance']) + html += mastodon_button_template(first, instance['instance']) first = false; } - html += another_button_template(); + html += mastodon_another_button_template(); - container.innerHTML = html; + mastodon_container.innerHTML = html; } replace_buttons(); diff --git a/templates/about.html b/templates/about.html index ede9d57..935b383 100644 --- a/templates/about.html +++ b/templates/about.html @@ -46,9 +46,35 @@ {% endif %} +

+ +

+{% for instance in misskey_instances %} + +{% if loop.first %} + {{picture(st, 'misskey', (20,40,80), ('webp', 'png'))}} + Log in with +{% endif %} + {{instance}} + +{% else %} + + {{picture(st, 'misskey', (20,40,80), ('webp', 'png'))}} + Log in with Misskey + +{% endfor %} + + +{% if misskey_instances %} + + Another Misskey instance + +{% endif %} +

+ - - + + + + + + + + + {% endif %} @@ -86,7 +143,7 @@
  • Delete your posts when they cross an age threshold.
  • Or keep your post count in check, deleting old posts when you go over.
  • -
  • Preserve old posts that matter by giving them a favourite.
  • +
  • Preserve old posts that matter by giving them a favourite or a reaction (Misskey only).
  • Set it and forget it. Forget works continuously in the background.
diff --git a/templates/misskey_login.html b/templates/misskey_login.html new file mode 100644 index 0000000..c830078 --- /dev/null +++ b/templates/misskey_login.html @@ -0,0 +1,29 @@ +{% extends 'lib/layout.html' %} +{% block body %} +
+

Log in with Misskey

+ +{% if generic_error %} + +{% endif %} + +{% if address_error %} + +{% endif %} + +
+ + + + +
+ + +
+{% endblock %} From ed1c42d30d0ff19be84e84e284977eda920fec6a Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 9 Nov 2021 10:07:56 +0100 Subject: [PATCH 03/22] WIP: implement misskey API --- libforget/misskey.py | 210 +++++++++++++++++++++++++++++++++++++++++++ model.py | 53 +++++++++++ routes/__init__.py | 67 ++++++++++++++ tasks.py | 37 ++++++++ 4 files changed, 367 insertions(+) create mode 100644 libforget/misskey.py diff --git a/libforget/misskey.py b/libforget/misskey.py new file mode 100644 index 0000000..d5d4c0a --- /dev/null +++ b/libforget/misskey.py @@ -0,0 +1,210 @@ +from app import db, sentry +from model import MisskeyApp, MisskeyInstance, Account, OAuthToken +from requests import get, post +from uuid import uuid4 +from hashlib import sha256 +from libforget.exceptions import TemporaryError, PermanentError + +def get_or_create_app(instance_url, callback, website): + instance_url = instance_url + app = MisskeyApp.query.get(instance_url) + + if not app: + # check if the instance uses https while getting instance infos + try: + r = post('https://{}/api/meta'.format(instance_url)) + r.raise_for_status() + proto = 'https' + except Exception: + r = post('http://{}/api/meta'.format(instance_url)) + r.raise_for_status() + proto = 'http' + + # check if miauth is available or we have to use legacy auth + miauth = 'miauth' in r.json()['features'] + + app = MisskeyApp() + app.instance = instance_url + app.protocol = proto + app.miauth = miauth + + if miauth: + # apps do not have to be registered for miauth + app.client_secret = None + else: + # register the app + r = post('{}://{}/api/app/create', json = { + 'name': 'forget', + 'description': website, + 'permission': ['read:favorites', 'write:notes'], + 'callbackUrl': callback + }) + r.raise_for_status() + app.client_secret = r.json()['secret'] + + return app + +def login_url(app, callback): + if app.miauth: + return "{}://{}/miauth/{}?name=forget&callback={}&permission=read:favorites,write:notes".format(app.protocol, app.instance, uuid4(), callback) + else: + # will use the callback we gave the server in `get_or_create_app` + r = post('{}://{}/api/auth/session/generate'.format(app.protocol, app.instance), json = { + 'appSecret': app.client_secret + }) + r.raise_for_status() + # we already get the retrieval token here, but we get it again later so + # we do not have to store it + return r.json()['url'] + +def receive_token(token, app): + if app.miauth: + r = get('{}://{}/api/miauth/{}/check'.format(app.protocol, app.instance, token)) + r.raise_for_status() + + token = r.json()['token'] + + acc = account_from_user(r.json()['user']) + acc = db.session.merge(acc) + token = OAuthToken(token = r.json()['token']) + token = db.session.merge(token) + token.account = acc + else: + r = post('{}://{}/api/auth/session/userkey'.format(app.protocol, app.instance), json = { + 'appSecret': app.client_secret, + 'token': token + }) + r.raise_for_status() + + token = sha256(r.json()['accessToken'].encode('utf-8') + app.client_secret.encode('utf-8')).hexdigest() + + acc = account_from_user(r.json()['user']) + acc = db.session.merge(acc) + token = OAuthToken(token = token) + token = db.session.merge(token) + token.account = acc + + return token + +def check_auth(account, app): + if app.miauth: + r = get('{}://{}/api/miauth/{}/check'.format(app.protocol, app.instance, account.token)) + + if r.status_code != 200: + raise TemporaryError("{} {}".format(r.status_code, r.body)) + + if not r.json()['ok']: + if sentry: + sentry.captureMessage( + 'Misskey auth revoked or incorrect', + extra=locals()) + db.session.delete(token) + db.session.commit() + raise PermanentError("Misskey auth revoked") + else: + # there is no such check for legacy auth, instead we check if we can + # get the user info + r = post('{}://{}/api/i'.format(app.protocol, app.instance), json = {'i': account.token}) + + if r.status_code != 200: + raise TemporaryError("{} {}".format(r.status_code, r.body)) + + if r.json()['isSuspended']: + # this is technically a temporary error, but like for twitter + # its handled as permanent to not make useless API calls + raise PermanentError("Misskey account suspended") + +def account_from_user(user): + return Account( + misskey_instance=user['host'], + misskey_id=user['id'], + screen_name='{}@{}'.format(user['username'], user['host']), + display_name=obj['name'], + avatar_url=obj['avatarUrl'], + reported_post_count=obj['notesCount'], + ) + +def post_from_api_object(obj): + return Post( + misskey_instance=user['host'], + misskey_id=user['id'], + favourite=obj['myReaction'] is not None, + has_media=('fileIds' in obj + and bool(obj['fileIds'])), + created_at=obj['createdAt'], + author_id=account_from_user(obj['user']).id, + direct=obj['visibility'] == 'specified', + is_reblog=obj['renoteId'] is not None, + ) + +def fetch_posts(acc, max_id, since_id): + app = MisskeyApp.query.get(acc.misskey_instance) + check_auth(acc, app) + if not verify_credentials(acc, app): + raise PermanentError() + try: + kwargs = dict(limit=40) + if max_id: + kwargs['untilId'] = max_id + if since_id: + kwargs['sinceId'] = since_id + + notes = post('{}://{}/api/users/notes'.format(app.protocol, app.misskey_instance), json=kwargs) + notes.raise_for_status() + + return [post_from_api_object(status) for note in notes.json()] + + except Exception as e: + raise TemporaryError(e) + + +def refresh_posts(posts): + acc = posts[0].author + app = MisskeyApp.query.get(acc.misskey_instance) + check_auth(acc, app) + + new_posts = list() + with db.session.no_autoflush: + for post in posts: + print('Refreshing {}'.format(post)) + r = post('{}://{}/api/notes/show'.format(app.protocol, app.misskey_instance), json={ + 'noteId': post.misskey_id + }) + if r.status_code != 200: + try: + if r.json()['error']['code'] == 'NO_SUCH_NOTE': + db.session.delete(post) + continue + except Exception as e: + raise TemporaryError(e) + raise TemporaryError('{} {}'.format(r.status_code, r.body)) + + new_post = db.session.merge(post_from_api_object(r.json())) + new_post.touch() + new_posts.append(new_post) + return new_posts + +def delete(post): + app = MisskeyApp.query.get(post.misskey_instance) + if not app: + # how? if this happens, it doesnt make sense to repeat it, + # so use a permanent error + raise PermanentError("instance not registered for delete") + + r = post('{}://{}/api/notes/delete'.format(app.protocol, app.misskey_instance), json = { + 'noteId': post.misskey_id + }) + + if r.status_code != 204: + raise TemporaryError("{} {}".format(r.status_code, r.body)) + + db.session.delete(post) + +def suggested_instances(limit=5, min_popularity=5, blocklist=tuple()): + return tuple((ins.instance for ins in ( + MisskeyInstance.query + .filter(MisskeyInstance.popularity > min_popularity) + .filter(~MisskeyInstance.instance.in_(blocklist)) + .order_by(db.desc(MisskeyInstance.popularity), + MisskeyInstance.instance) + .limit(limit).all()))) diff --git a/model.py b/model.py index 01cfd29..5951d6a 100644 --- a/model.py +++ b/model.py @@ -67,6 +67,34 @@ class RemoteIDMixin(object): @mastodon_id.setter def mastodon_id(self, id_): self.id = "mastodon:{}@{}".format(id_, self.mastodon_instance) + + @property + def misskey_instance(self): + if not self.id: + return None + if self.service != "misskey": + raise Exception( + "tried to get misskey instance for a {} {}" + .format(self.service, type(self))) + return self.id.split(":", 1)[1].split('@')[1] + + @misskey_instance.setter + def misskey_instance(self, instance): + self.id = "misskey:{}@{}".format(self.misskey_id, instance) + + @property + def misskey_id(self): + if not self.id: + return None + if self.service != "misskey": + raise Exception( + "tried to get misskey id for a {} {}" + .format(self.service, type(self))) + return self.id.split(":", 1)[1].split('@')[0] + + @misskey_id.setter + def misskey_id(self, id_): + self.id = "misskey:{}@{}".format(id_, self.misskey_instance) @property def remote_id(self): @@ -74,6 +102,8 @@ class RemoteIDMixin(object): return self.twitter_id elif self.service == 'mastodon': return self.mastodon_id + elif self.service == 'misskey': + return self.misskey_id ThreeWayPolicyEnum = db.Enum('keeponly', 'deleteonly', 'none', @@ -364,3 +394,26 @@ class MastodonInstance(db.Model): def bump(self, value=1): self.popularity = (self.popularity or 10) + value + +class MisskeyApp(db.Model, TimestampMixin): + __tablename__ = 'misskey_apps' + + instance = db.Column(db.String, primary_key=True) + protocol = db.Column(db.String, nullable=False) + miauth = db.Column(db.Boolean, nullable=False) + # only legacy auth uses client_secret + client_secret = db.Column(db.String, nullable=True) + +class MisskeyInstance(db.Model): + """ + this is for the autocomplete in the misskey login form + it isn't coupled with anything else so that we can seed it with + some popular instances ahead of time + """ + __tablename__ = 'misskey_instances' + + instance = db.Column(db.String, primary_key=True) + popularity = db.Column(db.Float, server_default='10', nullable=False) + + def bump(self, value=1): + self.popularity = (self.popularity or 10) + value diff --git a/routes/__init__.py b/routes/__init__.py index bfa2976..1e9fd5d 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -3,6 +3,7 @@ from flask import render_template, url_for, redirect, request, g,\ from datetime import datetime, timedelta, timezone import libforget.twitter import libforget.mastodon +import libforget.misskey from libforget.auth import require_auth, csrf,\ get_viewer from model import Session, TwitterArchive, MastodonApp @@ -240,6 +241,72 @@ def mastodon_login_step2(instance_url): return resp +@app.route('/login/misskey', methods=('GET', 'POST')) +def misskey_login(instance=None): + instance_url = (request.args.get('instance_url', None) + or request.form.get('instance_url', None)) + + if not instance_url: + instances = libforget.misskey.suggested_instances( + limit = 30, + min_popularity = 1 + ) + return render_template( + 'mastodon_login.html', instances=instances, + address_error=request.method == 'POST', + generic_error='error' in request.args + ) + + instance_url = instance_url.lower() + # strip protocol + instance_url = re.sub('^https?://', '', instance_url, + count=1, flags=re.IGNORECASE) + # strip username + instance_url = instance_url.split("@")[-1] + # strip trailing path + instance_url = instance_url.split('/')[0] + + callback = url_for('misskey_callback', + instance_url=instance_url, _external=True) + + try: + app = libforget.misskey.get_or_create_app( + instance_url, + callback_legacy, + url_for('index', _external=True)) + db.session.merge(app) + + db.session.commit() + + return redirect(libforget.misskey.login_url(app, callback)) + + except Exception: + if sentry: + sentry.captureException() + return redirect(url_for('misskey_login', error=True)) + + +@app.route('/login/misskey/callback/') +def misskey_callback(instance_url): + # legacy auth and miauth use different parameter names + token = request.args.get('token', None) or request.args.get('session', None) + app = MisskeyApp.query.get(instance_url) + if not token or not app: + return redirect(url_for('misskey_login', error=True)) + + token = libforget.misskey.receive_token(token, app) + account = token.account + + session = login(account.id) + + db.session.commit() + + g.viewer = session + + resp = redirect(url_for('index', _anchor='bump_instance')) + return resp + + @app.route('/sentry/setup.js') def sentry_setup(): client_dsn = app.config.get('SENTRY_DSN').split('@') diff --git a/tasks.py b/tasks.py index 07c7bf7..d26a757 100644 --- a/tasks.py +++ b/tasks.py @@ -5,6 +5,7 @@ from model import Session, Account, TwitterArchive, Post, OAuthToken,\ MastodonInstance import libforget.twitter import libforget.mastodon +import libforget.misskey from datetime import timedelta, datetime, timezone from time import time from zipfile import ZipFile @@ -151,6 +152,8 @@ def fetch_acc(id_): fetch_posts = libforget.twitter.fetch_posts elif (account.service == 'mastodon'): fetch_posts = libforget.mastodon.fetch_posts + elif (account.service == 'misskey'): + fetch_posts = libforget.misskey.fetch_posts posts = fetch_posts(account, max_id, since_id) if posts is None: @@ -291,6 +294,10 @@ def delete_from_account(account_id): if refreshed and is_eligible(refreshed[0]): to_delete = refreshed[0] break + elif account.service == 'misskey': + action = libforget.misskey.delete + posts = refresh_posts(posts) + to_delete = next(filter(is_eligible, posts), None) if to_delete: print("Deleting {}".format(to_delete)) @@ -317,6 +324,8 @@ def refresh_posts(posts): return libforget.twitter.refresh_posts(posts) elif posts[0].service == 'mastodon': return libforget.mastodon.refresh_posts(posts) + elif posts[0].service == 'misskey': + return libforget.misskey.refresh_posts(posts) @app.task() @@ -474,6 +483,33 @@ def update_mastodon_instances_popularity(): }) db.session.commit() +@app.task +def update_misskey_instances_popularity(): + # bump score for each active account + for acct in (Account.query.options(db.joinedload(Account.sessions)) + .filter(~Account.dormant).filter( + Account.id.like('misskey:%'))): + instance = MisskeyInstance.query.get(acct.misskey_instance) + if not instance: + instance = MisskeyInstance( + instance=acct.Misskey_instance, popularity=10) + db.session.add(instance) + amount = 0.01 + if acct.policy_enabled: + amount = 0.5 + for _ in acct.sessions: + amount += 0.1 + instance.bump(amount / max(1, instance.popularity)) + + # normalise scores so the top is 20 + top_pop = (db.session.query(db.func.max(MisskeyInstance.popularity)) + .scalar()) + MisskeyInstance.query.update({ + MisskeyInstance.popularity: + MisskeyInstance.popularity * 20 / top_pop + }) + db.session.commit() + app.add_periodic_task(40, queue_fetch_for_most_stale_accounts) app.add_periodic_task(9, queue_deletes) @@ -481,6 +517,7 @@ app.add_periodic_task(6, refresh_account_with_oldest_post) app.add_periodic_task(50, refresh_account_with_longest_time_since_refresh) app.add_periodic_task(300, periodic_cleanup) app.add_periodic_task(300, update_mastodon_instances_popularity) +app.add_periodic_task(300, update_misskey_instances_popularity) if __name__ == '__main__': app.worker_main() From b20395cc8f1c5e1e195f64c6b05a435a4389ab41 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 9 Nov 2021 10:08:30 +0100 Subject: [PATCH 04/22] add misskey-colored css --- assets/styles.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/styles.css b/assets/styles.css index b9dfc1a..c1dda3a 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -235,6 +235,10 @@ button { background-color: #282c37; } +.btn.primary.misskey-colored { + background-color: #66b300; +} + .btn.secondary { background-color: rgba(255,255,255,0.5); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.3); From 8b5f56bef2cccab184aac4925e90f6aaef4defec Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 9 Nov 2021 23:05:57 +0100 Subject: [PATCH 05/22] use urllib to parse instance url --- routes/__init__.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/routes/__init__.py b/routes/__init__.py index 1e9fd5d..5e4feef 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -11,6 +11,7 @@ from app import app, db, sentry, imgproxy import tasks from zipfile import BadZipFile from twitter import TwitterError +from urllib.parse import urlparse from urllib.error import URLError import libforget.version import libforget.settings @@ -172,6 +173,9 @@ def logout(): return redirect(url_for('about')) +def domain_from_url(url): + urlparse(url).netloc.lower() + @app.route('/login/mastodon', methods=('GET', 'POST')) def mastodon_login_step1(instance=None): @@ -189,14 +193,7 @@ def mastodon_login_step1(instance=None): generic_error='error' in request.args ) - instance_url = instance_url.lower() - # strip protocol - instance_url = re.sub('^https?://', '', instance_url, - count=1, flags=re.IGNORECASE) - # strip username - instance_url = instance_url.split("@")[-1] - # strip trailing path - instance_url = instance_url.split('/')[0] + instance_url = domain_from_url(instance_url) callback = url_for('mastodon_login_step2', instance_url=instance_url, _external=True) @@ -257,14 +254,7 @@ def misskey_login(instance=None): generic_error='error' in request.args ) - instance_url = instance_url.lower() - # strip protocol - instance_url = re.sub('^https?://', '', instance_url, - count=1, flags=re.IGNORECASE) - # strip username - instance_url = instance_url.split("@")[-1] - # strip trailing path - instance_url = instance_url.split('/')[0] + instance_url = domain_from_url(instance_url) callback = url_for('misskey_callback', instance_url=instance_url, _external=True) From 8214cda672c3714436d53bdf2cca4c61b07d637d Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 9 Nov 2021 23:06:44 +0100 Subject: [PATCH 06/22] fix IDs for instance JSON data --- templates/about.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/about.html b/templates/about.html index 935b383..aa6e3c4 100644 --- a/templates/about.html +++ b/templates/about.html @@ -76,7 +76,7 @@ -