2017-08-29 14:46:32 +02:00
|
|
|
from flask import render_template, url_for, redirect, request, g, Response,\
|
|
|
|
jsonify
|
2017-08-03 21:37:00 +02:00
|
|
|
from datetime import datetime, timedelta
|
2017-07-27 00:35:53 +02:00
|
|
|
import lib.twitter
|
2017-08-18 22:31:30 +02:00
|
|
|
import lib.mastodon
|
2017-08-01 20:57:15 +02:00
|
|
|
import lib
|
2017-08-25 10:50:11 +02:00
|
|
|
from lib.auth import require_auth, require_auth_api, csrf
|
2017-08-09 14:28:30 +02:00
|
|
|
from lib import set_session_cookie
|
2017-08-12 01:04:22 +02:00
|
|
|
from lib import get_viewer_session, get_viewer
|
2017-08-29 14:46:32 +02:00
|
|
|
from model import Session, TwitterArchive, MastodonApp, MastodonInstance
|
2017-08-10 17:07:39 +02:00
|
|
|
from app import app, db, sentry, limiter
|
2017-07-30 13:53:14 +02:00
|
|
|
import tasks
|
2017-08-07 15:35:15 +02:00
|
|
|
from zipfile import BadZipFile
|
2017-08-09 11:32:32 +02:00
|
|
|
from twitter import TwitterError
|
2017-08-09 14:27:39 +02:00
|
|
|
from urllib.error import URLError
|
2017-08-09 11:43:16 +02:00
|
|
|
import version
|
2017-08-20 18:48:43 +02:00
|
|
|
import lib.version
|
2017-07-25 09:52:24 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-07-27 01:17:28 +02:00
|
|
|
@app.before_request
|
|
|
|
def load_viewer():
|
2017-08-10 17:07:39 +02:00
|
|
|
g.viewer = get_viewer_session()
|
|
|
|
if g.viewer and sentry:
|
|
|
|
sentry.user_context({
|
|
|
|
'id': g.viewer.account.id,
|
|
|
|
'username': g.viewer.account.screen_name,
|
|
|
|
'service': g.viewer.account.service
|
|
|
|
})
|
2017-07-27 01:17:28 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-08-09 11:43:16 +02:00
|
|
|
@app.context_processor
|
|
|
|
def inject_version():
|
2017-08-20 18:48:43 +02:00
|
|
|
return dict(
|
|
|
|
version=version.version,
|
|
|
|
repo_url=lib.version.url_for_version(version.version),
|
|
|
|
)
|
2017-08-09 11:43:16 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-08-11 22:20:34 +02:00
|
|
|
@app.context_processor
|
|
|
|
def inject_sentry():
|
|
|
|
if sentry:
|
|
|
|
client_dsn = app.config.get('SENTRY_DSN').split('@')
|
|
|
|
client_dsn[:1] = client_dsn[0].split(':')
|
|
|
|
client_dsn = ':'.join(client_dsn[0:2]) + '@' + client_dsn[3]
|
|
|
|
return dict(sentry_dsn=client_dsn)
|
|
|
|
return dict()
|
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-07-27 01:17:28 +02:00
|
|
|
@app.after_request
|
|
|
|
def touch_viewer(resp):
|
2017-08-10 17:07:39 +02:00
|
|
|
if 'viewer' in g and g.viewer:
|
2017-08-09 14:28:30 +02:00
|
|
|
set_session_cookie(g.viewer, resp, app.config.get('HTTPS'))
|
2017-07-27 01:17:28 +02:00
|
|
|
g.viewer.touch()
|
|
|
|
db.session.commit()
|
|
|
|
return resp
|
|
|
|
|
2017-08-11 19:13:37 +02:00
|
|
|
|
2017-08-11 19:44:09 +02:00
|
|
|
lib.brotli.brotli(app)
|
2017-08-11 17:57:32 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-07-25 09:52:24 +02:00
|
|
|
@app.route('/')
|
2017-07-25 23:05:46 +02:00
|
|
|
def index():
|
2017-07-28 00:08:20 +02:00
|
|
|
if g.viewer:
|
2017-08-29 14:46:32 +02:00
|
|
|
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)
|
2017-07-28 00:08:20 +02:00
|
|
|
else:
|
2017-08-29 14:46:32 +02:00
|
|
|
instances = (
|
|
|
|
MastodonInstance.query
|
|
|
|
.filter(MastodonInstance.popularity > 13)
|
|
|
|
.order_by(db.desc(MastodonInstance.popularity),
|
|
|
|
MastodonInstance.instance)
|
|
|
|
.limit(5))
|
|
|
|
return render_template(
|
|
|
|
'index.html',
|
|
|
|
mastodon_instances=instances,
|
|
|
|
twitter_login_error='twitter_login_error' in request.args)
|
|
|
|
|
2017-07-25 09:52:24 +02:00
|
|
|
|
2017-07-25 23:05:46 +02:00
|
|
|
@app.route('/login/twitter')
|
2017-08-10 17:07:39 +02:00
|
|
|
@limiter.limit('3/minute')
|
2017-07-27 00:35:53 +02:00
|
|
|
def twitter_login_step1():
|
2017-08-08 16:18:39 +02:00
|
|
|
try:
|
|
|
|
return redirect(lib.twitter.get_login_url(
|
2017-08-29 14:46:32 +02:00
|
|
|
callback=url_for('twitter_login_step2', _external=True),
|
2017-08-08 16:18:39 +02:00
|
|
|
**app.config.get_namespace("TWITTER_")
|
|
|
|
))
|
|
|
|
except (TwitterError, URLError):
|
2017-08-29 16:41:11 +02:00
|
|
|
if sentry:
|
|
|
|
sentry.captureException()
|
2017-08-29 14:46:32 +02:00
|
|
|
return redirect(
|
|
|
|
url_for('index', twitter_login_error='', _anchor='log_in'))
|
|
|
|
|
2017-07-27 00:35:53 +02:00
|
|
|
|
|
|
|
@app.route('/login/twitter/callback')
|
2017-08-10 17:07:39 +02:00
|
|
|
@limiter.limit('3/minute')
|
2017-07-27 00:35:53 +02:00
|
|
|
def twitter_login_step2():
|
2017-08-08 16:18:39 +02:00
|
|
|
try:
|
|
|
|
oauth_token = request.args['oauth_token']
|
|
|
|
oauth_verifier = request.args['oauth_verifier']
|
2017-08-29 14:46:32 +02:00
|
|
|
token = lib.twitter.receive_verifier(
|
|
|
|
oauth_token, oauth_verifier,
|
|
|
|
**app.config.get_namespace("TWITTER_"))
|
2017-07-30 13:53:14 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
session = Session(account_id=token.account_id)
|
2017-08-08 16:18:39 +02:00
|
|
|
db.session.add(session)
|
|
|
|
db.session.commit()
|
2017-07-30 13:53:14 +02:00
|
|
|
|
2017-08-08 16:18:39 +02:00
|
|
|
tasks.fetch_acc.s(token.account_id).apply_async(routing_key='high')
|
2017-07-30 13:53:14 +02:00
|
|
|
|
2017-08-08 16:18:39 +02:00
|
|
|
resp = Response(status=302, headers={"location": url_for('index')})
|
2017-08-09 14:28:30 +02:00
|
|
|
set_session_cookie(session, resp, app.config.get('HTTPS'))
|
2017-08-08 16:18:39 +02:00
|
|
|
return resp
|
|
|
|
except (TwitterError, URLError):
|
2017-08-29 16:41:11 +02:00
|
|
|
if sentry:
|
|
|
|
sentry.captureException()
|
2017-08-29 14:46:32 +02:00
|
|
|
return redirect(
|
|
|
|
url_for('index', twitter_login_error='', _anchor='log_in'))
|
|
|
|
|
2017-07-25 23:05:46 +02:00
|
|
|
|
2017-08-29 13:26:32 +02:00
|
|
|
class TweetArchiveEmptyException(Exception):
|
|
|
|
pass
|
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-07-31 04:51:11 +02:00
|
|
|
@app.route('/upload_tweet_archive', methods=('POST',))
|
2017-08-10 17:07:39 +02:00
|
|
|
@limiter.limit('10/10 minutes')
|
2017-07-31 04:51:11 +02:00
|
|
|
@require_auth
|
|
|
|
def upload_tweet_archive():
|
2017-08-29 14:46:32 +02:00
|
|
|
ta = TwitterArchive(
|
|
|
|
account=g.viewer.account,
|
|
|
|
body=request.files['file'].read())
|
2017-07-31 00:07:34 +02:00
|
|
|
db.session.add(ta)
|
|
|
|
db.session.commit()
|
|
|
|
|
2017-08-07 15:35:15 +02:00
|
|
|
try:
|
2017-08-12 20:32:51 +02:00
|
|
|
files = lib.twitter.chunk_twitter_archive(ta.id)
|
|
|
|
|
|
|
|
ta.chunks = len(files)
|
|
|
|
db.session.commit()
|
2017-07-31 00:07:34 +02:00
|
|
|
|
2017-08-29 13:26:32 +02:00
|
|
|
if not ta.chunks > 0:
|
|
|
|
raise TweetArchiveEmptyException()
|
2017-08-07 15:35:15 +02:00
|
|
|
|
2017-08-12 20:32:51 +02:00
|
|
|
for filename in files:
|
2017-08-18 23:03:49 +02:00
|
|
|
tasks.import_twitter_archive_month.s(ta.id, filename).apply_async()
|
2017-08-12 20:32:51 +02:00
|
|
|
|
2017-08-07 15:35:15 +02:00
|
|
|
return redirect(url_for('index', _anchor='recent_archives'))
|
2017-08-29 13:26:32 +02:00
|
|
|
except (BadZipFile, TweetArchiveEmptyException):
|
2017-08-29 16:41:11 +02:00
|
|
|
if sentry:
|
|
|
|
sentry.captureException()
|
2017-08-29 14:46:32 +02:00
|
|
|
return redirect(
|
|
|
|
url_for('index', tweet_archive_failed='',
|
|
|
|
_anchor='tweet_archive_import'))
|
|
|
|
|
2017-07-31 04:51:11 +02:00
|
|
|
|
2017-08-03 16:05:28 +02:00
|
|
|
@app.route('/settings', methods=('POST',))
|
2017-08-25 10:50:11 +02:00
|
|
|
@csrf
|
2017-07-31 04:51:11 +02:00
|
|
|
@require_auth
|
|
|
|
def settings():
|
2017-08-12 12:26:35 +02:00
|
|
|
viewer = get_viewer()
|
|
|
|
try:
|
|
|
|
for attr in lib.settings.attrs:
|
2017-08-07 16:26:25 +02:00
|
|
|
if attr in request.form:
|
2017-08-12 12:26:35 +02:00
|
|
|
setattr(viewer, attr, request.form[attr])
|
|
|
|
db.session.commit()
|
|
|
|
except ValueError:
|
2017-08-29 16:41:11 +02:00
|
|
|
if sentry:
|
|
|
|
sentry.captureException()
|
2017-08-12 12:26:35 +02:00
|
|
|
return 400
|
2017-07-31 18:29:09 +02:00
|
|
|
|
2017-08-03 16:05:28 +02:00
|
|
|
return redirect(url_for('index', settings_saved=''))
|
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-08-03 16:05:28 +02:00
|
|
|
@app.route('/disable', methods=('POST',))
|
2017-08-25 10:50:11 +02:00
|
|
|
@csrf
|
2017-08-03 16:05:28 +02:00
|
|
|
@require_auth
|
|
|
|
def disable():
|
|
|
|
g.viewer.account.policy_enabled = False
|
|
|
|
db.session.commit()
|
2017-07-31 18:29:09 +02:00
|
|
|
|
2017-08-03 16:05:28 +02:00
|
|
|
return redirect(url_for('index'))
|
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-08-03 16:05:28 +02:00
|
|
|
@app.route('/enable', methods=('POST',))
|
2017-08-25 10:50:11 +02:00
|
|
|
@csrf
|
2017-08-03 16:05:28 +02:00
|
|
|
@require_auth
|
|
|
|
def enable():
|
2017-08-29 14:46:32 +02:00
|
|
|
if 'confirm' not in request.form and not g.viewer.account.policy_enabled:
|
2017-08-03 21:37:00 +02:00
|
|
|
if g.viewer.account.policy_delete_every == timedelta(0):
|
|
|
|
approx = g.viewer.account.estimate_eligible_for_delete()
|
2017-08-29 14:46:32 +02:00
|
|
|
return render_template(
|
|
|
|
'warn.html',
|
|
|
|
message=f"""
|
|
|
|
You've set the time between deleting posts to 0. Every post
|
|
|
|
that matches your expiration rules will be deleted within
|
|
|
|
minutes.
|
|
|
|
{ ("That's about " + str(approx) + " posts.") if approx > 0
|
|
|
|
else "" }
|
|
|
|
Go ahead?
|
|
|
|
""")
|
2017-08-14 20:58:22 +02:00
|
|
|
if g.viewer.account.next_delete < datetime.now() - timedelta(days=365):
|
2017-08-29 14:46:32 +02:00
|
|
|
return render_template(
|
|
|
|
'warn.html',
|
|
|
|
message="""
|
|
|
|
Once you enable Forget, posts that match your
|
|
|
|
expiration rules will be deleted <b>permanently</b>.
|
|
|
|
We can't bring them back. Make sure that you won't
|
|
|
|
miss them.
|
|
|
|
""")
|
2017-08-03 21:37:00 +02:00
|
|
|
|
|
|
|
if not g.viewer.account.policy_enabled:
|
2017-08-29 14:46:32 +02:00
|
|
|
g.viewer.account.next_delete = (
|
|
|
|
datetime.now() + g.viewer.account.policy_delete_every)
|
2017-08-03 21:37:00 +02:00
|
|
|
|
2017-08-03 16:05:28 +02:00
|
|
|
g.viewer.account.policy_enabled = True
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
return redirect(url_for('index'))
|
2017-07-31 18:29:09 +02:00
|
|
|
|
|
|
|
|
2017-07-25 23:05:46 +02:00
|
|
|
@app.route('/logout')
|
2017-07-31 04:51:11 +02:00
|
|
|
@require_auth
|
2017-07-25 23:05:46 +02:00
|
|
|
def logout():
|
2017-07-27 01:17:28 +02:00
|
|
|
if(g.viewer):
|
|
|
|
db.session.delete(g.viewer)
|
|
|
|
db.session.commit()
|
2017-07-27 14:19:40 +02:00
|
|
|
g.viewer = None
|
2017-07-25 23:05:46 +02:00
|
|
|
return redirect(url_for('index'))
|
2017-08-11 22:20:24 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-08-12 01:04:22 +02:00
|
|
|
@app.route('/api/settings', methods=('PUT',))
|
|
|
|
@require_auth_api
|
|
|
|
def api_settings_put():
|
|
|
|
viewer = get_viewer()
|
|
|
|
data = request.json
|
|
|
|
updated = dict()
|
|
|
|
for key in lib.settings.attrs:
|
|
|
|
if key in data:
|
|
|
|
setattr(viewer, key, data[key])
|
|
|
|
updated[key] = data[key]
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify(status='success', updated=updated)
|
2017-08-12 01:52:33 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-08-12 01:52:33 +02:00
|
|
|
@app.route('/api/viewer')
|
|
|
|
@require_auth_api
|
2017-08-16 00:23:41 +02:00
|
|
|
def api_viewer():
|
2017-08-12 01:52:33 +02:00
|
|
|
viewer = get_viewer()
|
|
|
|
return jsonify(
|
|
|
|
post_count=viewer.post_count(),
|
|
|
|
eligible_for_delete_estimate=viewer.estimate_eligible_for_delete(),
|
2017-08-12 12:26:35 +02:00
|
|
|
display_name=viewer.display_name,
|
|
|
|
screen_name=viewer.screen_name,
|
|
|
|
avatar_url=viewer.avatar_url,
|
|
|
|
id=viewer.id,
|
|
|
|
service=viewer.service,
|
2017-08-16 00:23:41 +02:00
|
|
|
)
|
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-08-16 00:23:41 +02:00
|
|
|
@app.route('/api/viewer/timers')
|
|
|
|
@require_auth_api
|
|
|
|
def api_viewer_timers():
|
|
|
|
viewer = get_viewer()
|
|
|
|
return jsonify(
|
2017-08-12 23:07:16 +02:00
|
|
|
last_refresh=viewer.last_refresh,
|
|
|
|
last_refresh_rel=lib.interval.relnow(viewer.last_refresh),
|
|
|
|
last_fetch=viewer.last_fetch,
|
|
|
|
last_fetch_rel=lib.interval.relnow(viewer.last_fetch),
|
2017-08-14 20:58:22 +02:00
|
|
|
next_delete=viewer.next_delete,
|
|
|
|
next_delete_rel=lib.interval.relnow(viewer.next_delete),
|
2017-08-12 01:52:33 +02:00
|
|
|
)
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-08-18 22:31:30 +02:00
|
|
|
@app.route('/login/mastodon', methods=('GET', 'POST'))
|
2017-08-23 15:14:24 +02:00
|
|
|
def mastodon_login_step1(instance=None):
|
2017-08-23 11:42:32 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
instance_url = (request.args.get('instance_url', None)
|
|
|
|
or request.form.get('instance_url', None))
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-23 15:14:24 +02:00
|
|
|
if not instance_url:
|
2017-08-29 16:41:11 +02:00
|
|
|
instances = (
|
|
|
|
MastodonInstance
|
|
|
|
.query.filter(MastodonInstance.popularity > 1)
|
|
|
|
.order_by(db.desc(MastodonInstance.popularity),
|
|
|
|
MastodonInstance.instance)
|
|
|
|
.limit(30))
|
2017-08-29 14:46:32 +02:00
|
|
|
return render_template(
|
|
|
|
'mastodon_login.html', instances=instances,
|
|
|
|
address_error=request.method == 'POST',
|
|
|
|
generic_error='error' in request.args
|
2017-08-23 15:14:24 +02:00
|
|
|
)
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-23 15:14:24 +02:00
|
|
|
instance_url = instance_url.split("@")[-1].lower()
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
callback = url_for('mastodon_login_step2',
|
2017-08-29 16:45:00 +02:00
|
|
|
instance_url=instance_url, _external=True)
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-29 16:41:11 +02:00
|
|
|
try:
|
|
|
|
app = lib.mastodon.get_or_create_app(
|
|
|
|
instance_url,
|
|
|
|
callback,
|
|
|
|
url_for('index', _external=True))
|
|
|
|
db.session.merge(app)
|
|
|
|
|
|
|
|
db.session.commit()
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-29 16:41:11 +02:00
|
|
|
return redirect(lib.mastodon.login_url(app, callback))
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-29 16:41:11 +02:00
|
|
|
except Exception:
|
|
|
|
if sentry:
|
|
|
|
sentry.captureException()
|
|
|
|
return redirect(url_for('mastodon_login_step1', error=True))
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
|
2017-08-29 16:45:00 +02:00
|
|
|
@app.route('/login/mastodon/callback/<instance_url>')
|
2017-08-29 14:46:32 +02:00
|
|
|
def mastodon_login_step2(instance_url):
|
2017-08-18 22:31:30 +02:00
|
|
|
code = request.args.get('code', None)
|
2017-08-29 14:46:32 +02:00
|
|
|
app = MastodonApp.query.get(instance_url)
|
2017-08-18 22:31:30 +02:00
|
|
|
if not code or not app:
|
2017-08-29 16:31:43 +02:00
|
|
|
return redirect(url_for('mastodon_login_step1', error=True))
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
callback = url_for('mastodon_login_step2',
|
2017-08-29 16:45:00 +02:00
|
|
|
instance_url=instance_url, _external=True)
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-19 14:32:31 +02:00
|
|
|
token = lib.mastodon.receive_code(code, app, callback)
|
|
|
|
account = token.account
|
2017-08-18 22:31:30 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
sess = Session(account=account)
|
2017-08-18 22:31:30 +02:00
|
|
|
db.session.add(sess)
|
2017-08-23 11:42:32 +02:00
|
|
|
|
2017-08-29 14:46:32 +02:00
|
|
|
instance = MastodonInstance(instance=instance_url)
|
|
|
|
instance = db.session.merge(instance)
|
|
|
|
instance.bump()
|
2017-08-23 11:42:32 +02:00
|
|
|
|
2017-08-18 22:31:30 +02:00
|
|
|
db.session.commit()
|
|
|
|
|
2017-08-23 13:44:15 +02:00
|
|
|
tasks.fetch_acc.s(account.id).apply_async(routing_key='high')
|
|
|
|
|
2017-08-18 22:31:30 +02:00
|
|
|
g.viewer = sess
|
|
|
|
return redirect(url_for('index'))
|