From d3d93c3cef116c19bbdb6e9a3cf8791b39897275 Mon Sep 17 00:00:00 2001 From: codl Date: Fri, 18 Aug 2017 22:31:30 +0200 Subject: [PATCH] ghfjklghjdkflhgjfklhgjkdflshgjdflshgjkdflshgjdfklshgjkdflghjkdflhgjkldfshjgkldfhjgldhfjkgldhfjgklfdhsjgklhfjkslghjdfklsg mastodon why --- assets/styles.css | 12 ++- lib/mastodon.py | 88 +++++++++++++++++++ .../7afc7b343323_add_mastodon_apps.py | 32 +++++++ ...c80af843eed3_make_token_secret_nullable.py | 28 ++++++ ...b29df9_it_s_supposed_to_be_plural_dummy.py | 24 +++++ model.py | 41 +++++++-- requirements.txt | 7 ++ routes.py | 68 +++++++++++++- tasks.py | 9 +- templates/index.html | 5 +- templates/mastodon_login.html | 26 ++++++ 11 files changed, 324 insertions(+), 16 deletions(-) create mode 100644 lib/mastodon.py create mode 100644 migrations/versions/7afc7b343323_add_mastodon_apps.py create mode 100644 migrations/versions/c80af843eed3_make_token_secret_nullable.py create mode 100644 migrations/versions/fbdc10b29df9_it_s_supposed_to_be_plural_dummy.py create mode 100644 templates/mastodon_login.html diff --git a/assets/styles.css b/assets/styles.css index 1505fcc..4fcdc22 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -1,11 +1,11 @@ body { margin: 0; font-family: sans-serif; - line-height: 1.5em; } *, *::before, *::after { box-sizing: border-box; + line-height: 1.5em; } body > section, body > header, body > footer { @@ -54,6 +54,10 @@ input[type=number]{ max-width: 8ch; } +input, select, button { + line-height: 1em; +} + .viewer img.avatar { height: 1.5em; width: 1.5em; @@ -182,5 +186,7 @@ footer a { } } - - +form aside { + font-style: italic; + font-size: 0.8em; +} diff --git a/lib/mastodon.py b/lib/mastodon.py new file mode 100644 index 0000000..539fefb --- /dev/null +++ b/lib/mastodon.py @@ -0,0 +1,88 @@ +import mastodon +from mastodon import Mastodon +from model import MastodonApp, Account, OAuthToken +from requests import head +from app import db + +def get_or_create_app(instance_url, callback, website): + instance_url = instance_url + app = MastodonApp.query.get(instance_url) + try: + head('https://{}'.format(instance_url)).raise_for_status() + proto = 'https' + except Exception: + head('http://{}'.format(instance_url)).raise_for_status() + proto = 'http' + + if not app: + client_id, client_secret = Mastodon.create_app('forget', + scopes=('read', 'write'), + api_base_url='{}://{}'.format(proto, instance_url), + redirect_uris=callback, + website=website, + ) + app = MastodonApp() + app.instance = instance_url + app.client_id = client_id + app.client_secret = client_secret + app.protocol = proto + return app + +def anonymous_api(app): + return Mastodon(app.client_id, + client_secret = app.client_secret, + api_base_url='{}://{}'.format(app.protocol, app.instance), + ) + +def login_url(app, callback): + return anonymous_api(app).auth_request_url( + redirect_uris=callback, + scopes=('read', 'write',) + ) + +def receive_code(code, app, callback): + api = anonymous_api(app) + access_token = api.log_in( + code=code, + scopes=('read', 'write'), + redirect_uri=callback, + ) + + remote_acc = api.account_verify_credentials() + acc = Account( + #id = 'mastodon:{}:{}'.format(app.instance, remote_acc['username']), + mastodon_instance = app.instance, + mastodon_id = remote_acc['username'], + screen_name = remote_acc['username'], + display_name = remote_acc['display_name'], + avatar_url = remote_acc['avatar'], + reported_post_count = remote_acc['statuses_count'], + ) + token = OAuthToken(account = acc, token = access_token) + db.session.merge(acc, token) + + return acc + + +def get_api_for_acc(account): + app = MastodonApp.query.get(account.mastodon_instance) + for token in account.tokens: + api = Mastodon(app.client_id, + client_secret = app.client_secret, + api_base_url = '{}://{}'.format(app.protocol, app.instance), + access_token = token.token, + ) + try: + # api.verify_credentials() + # doesnt error even if the token is revoked lol sooooo + tl = api.timeline() + #if 'error' in tl and tl['error'] == 'The access token was revoked': + #ARRRRRGH + except mastodon.MastodonAPIError as e: + raise e + return api + + +def fetch_acc(account, cursor=None): + pass + diff --git a/migrations/versions/7afc7b343323_add_mastodon_apps.py b/migrations/versions/7afc7b343323_add_mastodon_apps.py new file mode 100644 index 0000000..7f4d958 --- /dev/null +++ b/migrations/versions/7afc7b343323_add_mastodon_apps.py @@ -0,0 +1,32 @@ +"""add mastodon apps + +Revision ID: 7afc7b343323 +Revises: f63bf9e73bc9 +Create Date: 2017-08-18 20:36:00.104508 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '7afc7b343323' +down_revision = 'f63bf9e73bc9' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('mastodon_app', + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('instance', sa.String(), nullable=False), + sa.Column('client_id', sa.String(), nullable=False), + sa.Column('client_secret', sa.String(), nullable=False), + sa.Column('protocol', sa.Enum('http', 'https', name='enum_protocol'), nullable=False), + sa.PrimaryKeyConstraint('instance', name=op.f('pk_mastodon_app')) + ) + + +def downgrade(): + op.drop_table('mastodon_app') diff --git a/migrations/versions/c80af843eed3_make_token_secret_nullable.py b/migrations/versions/c80af843eed3_make_token_secret_nullable.py new file mode 100644 index 0000000..d144aa2 --- /dev/null +++ b/migrations/versions/c80af843eed3_make_token_secret_nullable.py @@ -0,0 +1,28 @@ +"""make token secret nullable + +Revision ID: c80af843eed3 +Revises: fbdc10b29df9 +Create Date: 2017-08-18 21:25:17.933702 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c80af843eed3' +down_revision = 'fbdc10b29df9' +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column('oauth_tokens', 'token_secret', + existing_type=sa.VARCHAR(), + nullable=True) + + +def downgrade(): + op.alter_column('oauth_tokens', 'token_secret', + existing_type=sa.VARCHAR(), + nullable=False) diff --git a/migrations/versions/fbdc10b29df9_it_s_supposed_to_be_plural_dummy.py b/migrations/versions/fbdc10b29df9_it_s_supposed_to_be_plural_dummy.py new file mode 100644 index 0000000..5900b03 --- /dev/null +++ b/migrations/versions/fbdc10b29df9_it_s_supposed_to_be_plural_dummy.py @@ -0,0 +1,24 @@ +"""it's supposed to be plural, dummy + +Revision ID: fbdc10b29df9 +Revises: 7afc7b343323 +Create Date: 2017-08-18 20:39:39.119165 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'fbdc10b29df9' +down_revision = '7afc7b343323' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute('ALTER TABLE mastodon_app RENAME TO mastodon_apps') + + +def downgrade(): + op.execute('ALTER TABLE mastodon_apps RENAME TO mastodon_app') diff --git a/model.py b/model.py index bc89574..8d82e80 100644 --- a/model.py +++ b/model.py @@ -1,11 +1,8 @@ -from datetime import datetime +from datetime import timedelta, datetime from app import db - -from twitter import Twitter, OAuth import secrets from lib import decompose_interval -from datetime import timedelta, datetime class TimestampMixin(object): created_at = db.Column(db.DateTime, server_default=db.func.now(), nullable=False) @@ -33,6 +30,30 @@ class RemoteIDMixin(object): def twitter_id(self, id): self.id = "twitter:{}".format(id) + @property + def mastodon_instance(self): + if not self.id: + return None + if self.service != "mastodon": + raise Exception("tried to get mastodon instance for a {} {}".format(self.service, type(self))) + return self.id.split(":", 1)[1].split('@')[1] + + @mastodon_instance.setter + def mastodon_instance(self, instance): + self.id = "mastodon:{}@{}".format(self.mastodon_id, instance) + + @property + def mastodon_id(self): + if not self.id: + return None + if self.service != "mastodon": + raise Exception("tried to get mastodon id for a {} {}".format(self.service, type(self))) + return self.id.split(":", 1)[1].split('@')[0] + + @mastodon_id.setter + def mastodon_id(self, id): + self.id = "mastodon:{}@{}".format(id, self.mastodon_instance) + @decompose_interval('policy_delete_every') @decompose_interval('policy_keep_younger') @@ -123,7 +144,7 @@ class OAuthToken(db.Model, TimestampMixin): __tablename__ = 'oauth_tokens' token = db.Column(db.String, primary_key=True) - token_secret = db.Column(db.String, nullable=False) + token_secret = db.Column(db.String, nullable=True) account_id = db.Column(db.String, db.ForeignKey('accounts.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=True, index=True) account = db.relationship(Account, backref=db.backref('tokens', order_by=lambda: db.desc(OAuthToken.created_at))) @@ -181,3 +202,13 @@ class TwitterArchive(db.Model, TimestampMixin): if self.chunks_successful == self.chunks: return 'successful' return 'pending' + +ProtoEnum = db.Enum('http', 'https', name='enum_protocol') + +class MastodonApp(db.Model, TimestampMixin): + __tablename__ = 'mastodon_apps' + + instance = db.Column(db.String, primary_key=True) + client_id = db.Column(db.String, nullable=False) + client_secret = db.Column(db.String, nullable=False) + protocol = db.Column(ProtoEnum, nullable=False) diff --git a/requirements.txt b/requirements.txt index 2e7575b..c2fe50b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,10 +3,13 @@ amqp==2.2.1 billiard==3.5.0.3 Brotli==0.6.0 celery==4.1.0 +certifi==2017.7.27.1 +chardet==3.0.4 click==6.7 cloudpickle==0.4.0 contextlib2==0.5.5 csscompressor==0.9.4 +dateutils==0.6.6 doit==0.30.3 Flask==0.12.2 Flask-Limiter==0.9.5 @@ -14,12 +17,14 @@ Flask-Migrate==2.1.0 Flask-SQLAlchemy==2.2 gunicorn==19.7.1 honcho==1.0.1 +idna==2.6 itsdangerous==0.24 Jinja2==2.9.6 kombu==4.1.0 limits==1.2.1 Mako==1.0.7 MarkupSafe==1.0 +Mastodon.py==1.0.8 olefile==0.44 Pillow==4.2.1 psycopg2==2.7.3 @@ -29,8 +34,10 @@ python-editor==1.0.3 pytz==2017.2 raven==6.1.0 redis==2.10.5 +requests==2.18.4 six==1.10.0 SQLAlchemy==1.1.13 twitter==1.17.1 +urllib3==1.22 vine==1.1.4 Werkzeug==0.12.2 diff --git a/routes.py b/routes.py index 3c4d905..588dd5a 100644 --- a/routes.py +++ b/routes.py @@ -1,20 +1,18 @@ from flask import render_template, url_for, redirect, request, g, Response, jsonify from datetime import datetime, timedelta import lib.twitter +import lib.mastodon import lib from lib.auth import require_auth, require_auth_api from lib import set_session_cookie from lib import get_viewer_session, get_viewer -from model import Account, Session, Post, TwitterArchive +from model import Account, Session, Post, TwitterArchive, MastodonApp from app import app, db, sentry, limiter import tasks from zipfile import BadZipFile from twitter import TwitterError from urllib.error import URLError import version -import lib.brotli -import lib.settings -import lib.interval @app.before_request def load_viewer(): @@ -53,6 +51,19 @@ lib.brotli.brotli(app) @app.route('/') def index(): if g.viewer: + if g.viewer.account.service == 'mastodon': + import lib.mastodon + api = lib.mastodon.get_api_for_acc(g.viewer.account) + me = api.account_verify_credentials() + #return str(me) + tl = api.timeline() + return str(tl) + if not api: + raise Exception('frick!!!!!') + else: + raise Exception(api) + + return render_template('logged_in.html', scales=lib.interval.SCALES, tweet_archive_failed = 'tweet_archive_failed' in request.args, settings_error = 'settings_error' in request.args @@ -212,3 +223,52 @@ def api_viewer_timers(): next_delete=viewer.next_delete, next_delete_rel=lib.interval.relnow(viewer.next_delete), ) + +@app.route('/login/mastodon', methods=('GET', 'POST')) +def mastodon_login_step1(): + if request.method == 'GET': + return render_template('mastodon_login.html', generic_error = 'error' in request.args) + + if not 'instance_url' in request.form or not request.form['instance_url']: + return render_template('mastodon_login.html', address_error=True) + + instance_url = request.form['instance_url'].split("@")[-1].lower() + + callback = url_for('mastodon_login_step2', instance=instance_url, _external=True) + + app = lib.mastodon.get_or_create_app(instance_url, + callback, + url_for('index', _external=True)) + db.session.merge(app) + + db.session.commit() + + return redirect(lib.mastodon.login_url(app, callback)) + +@app.route('/login/mastodon/callback/') +def mastodon_login_step2(instance): + code = request.args.get('code', None) + app = MastodonApp.query.get(instance) + if not code or not app: + return redirect('mastodon_login_step1', error=True) + + callback = url_for('mastodon_login_step2', instance=instance, _external=True) + + account = lib.mastodon.receive_code(code, app, callback) + + account = db.session.merge(account) + + sess = Session(account = account) + db.session.add(sess) + db.session.commit() + + g.viewer = sess + return redirect(url_for('index')) + + +@app.route('/debug') +def debug(): + sess = Session(account = Account.query.filter_by(display_name = 'codltest').first()) + db.session.merge(sess) + db.session.commit() + return sess.id diff --git a/tasks.py b/tasks.py index 901cdb0..8d7993b 100644 --- a/tasks.py +++ b/tasks.py @@ -47,9 +47,12 @@ def fetch_acc(id, cursor=None): print(f'fetching {acc}') try: if(acc.service == 'twitter'): - cursor = lib.twitter.fetch_acc(acc, cursor, **flaskapp.config.get_namespace("TWITTER_")) - if cursor: - fetch_acc.si(id, cursor).apply_async() + action = lib.twitter.fetch_acc + elif(acc.service == 'mastodon'): + action = lib.mastodon.fetch_acc + cursor = action(acc, cursor, **flaskapp.config.get_namespace("TWITTER_")) + if cursor: + fetch_acc.si(id, cursor).apply_async() finally: db.session.rollback() acc.touch_fetch() diff --git a/templates/index.html b/templates/index.html index 3d42c55..4e5bb9f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -27,7 +27,10 @@ {% endif %} -

Sound good to you? Log in with Twitter

+

Sound good to you? + Log in with Twitter + Log in with Mastodon +

Policy

diff --git a/templates/mastodon_login.html b/templates/mastodon_login.html new file mode 100644 index 0000000..14bce51 --- /dev/null +++ b/templates/mastodon_login.html @@ -0,0 +1,26 @@ +{% extends 'lib/layout.html' %} +{% block body %} +
+

Log in with Mastodon

+ +{% if generic_error %} + +{% endif %} + +{% if address_error %} + +{% endif %} + +
+ + + +
+ + +
+{% endblock %}