ghfjklghjdkflhgjfklhgjkdflshgjdflshgjkdflshgjdfklshgjkdflghjkdflhgjkldfshjgkldfhjgldhfjkgldhfjgklfdhsjgklhfjkslghjdfklsg

mastodon why
This commit is contained in:
codl 2017-08-18 22:31:30 +02:00
parent 340eb711d5
commit d3d93c3cef
No known key found for this signature in database
GPG Key ID: 6CD7C8891ED1233A
11 changed files with 324 additions and 16 deletions

View File

@ -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;
}

88
lib/mastodon.py Normal file
View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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/<instance>')
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

View File

@ -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()

View File

@ -27,7 +27,10 @@
</div>
{% endif %}
<p>Sound good to you? <a href="/login/twitter">Log in with Twitter</a></p>
<p>Sound good to you?
<a href="/login/twitter">Log in with Twitter</a>
<a href="{{ url_for('mastodon_login_step1') }}">Log in with Mastodon</a>
</p>
</section>
<section class='policy'>
<h2>Policy</h2>

View File

@ -0,0 +1,26 @@
{% extends 'lib/layout.html' %}
{% block body %}
<section>
<h2>Log in with Mastodon</h2>
{% if generic_error %}
<div class='banner error'>Something went wrong while logging in. Try again?</div>
{% endif %}
{% if address_error %}
<div class='banner error'>This doesn't look like a mastodon instance url. Try again?</div>
{% endif %}
<form method='post'>
<label>Your mastodon address:
<input type='text' name='instance_url' placeholder='example@glitch.social'/>
</label>
<input name='confirm' value='Log in' type='submit'/>
<aside>
Your mastodon address is your username, followed by an @ sign and the address of your mastodon instance.
</aside>
</form>
</div>
</section>
{% endblock %}