kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
b98e577656
commit
a8cddbfedd
153
pages.py
153
pages.py
|
@ -1,5 +1,6 @@
|
|||
"""UI pages."""
|
||||
import datetime
|
||||
from functools import wraps
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
|
@ -10,7 +11,7 @@ from flask import render_template, request
|
|||
from google.cloud.ndb import tasklets
|
||||
from google.cloud.ndb.key import Key
|
||||
from google.cloud.ndb.query import OR
|
||||
from google.cloud.ndb.model import get_multi
|
||||
from google.cloud.ndb.model import get_multi, Model
|
||||
from granary import as1, as2, atom, microformats2, rss
|
||||
import oauth_dropins
|
||||
import oauth_dropins.bluesky
|
||||
|
@ -19,6 +20,8 @@ from oauth_dropins.webutil.flask_util import (
|
|||
canonicalize_request_domain,
|
||||
error,
|
||||
flash,
|
||||
get_required_param,
|
||||
Found,
|
||||
)
|
||||
from oauth_dropins.webutil.util import json_loads, json_dumps
|
||||
import requests
|
||||
|
@ -31,7 +34,7 @@ from activitypub import ActivityPub, instance_actor
|
|||
import atproto
|
||||
from atproto import ATProto, BlueskyOAuthStart
|
||||
import common
|
||||
from common import CACHE_CONTROL, DOMAIN_RE, PROTOCOL_DOMAINS
|
||||
from common import CACHE_CONTROL, DOMAIN_RE, ErrorButDoNotRetryTask, PROTOCOL_DOMAINS
|
||||
from flask_app import app
|
||||
from flask import redirect, session
|
||||
import ids
|
||||
|
@ -128,6 +131,31 @@ def load_user(protocol, id):
|
|||
error(f'{protocol} user {id} not found', status=404)
|
||||
|
||||
|
||||
def require_login(fn):
|
||||
"""Decorator that requires and loads the current request's logged in user.
|
||||
|
||||
Passes the userin the ``user`` kwarg, as a :class:`models.User`.
|
||||
|
||||
HTTP POST params:
|
||||
key (str): url-safe ndb key
|
||||
|
||||
Raises:
|
||||
:class:`werkzeug.exceptions.HTTPException` on error or redirect
|
||||
"""
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
key = Key(urlsafe=get_required_param('key'))
|
||||
if key not in [login_to_user_key(l) for l in get_logins()]:
|
||||
logger.warning(f'failed login attempt for {key}')
|
||||
raise Found('/login')
|
||||
elif not (user := key.get()):
|
||||
raise Found('/login')
|
||||
|
||||
return fn(*args, user=user, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def get_logins():
|
||||
"""Returns the user's current logged in sessions:
|
||||
|
||||
|
@ -138,6 +166,30 @@ def get_logins():
|
|||
return sorted(logins, key=lambda l: (l.key.kind(), l.user_display_name()))
|
||||
|
||||
|
||||
def login_to_user_key(login):
|
||||
""""Converts an oauth-dropins auth entity to a :model:`User` key.
|
||||
|
||||
Args:
|
||||
login (oauth_dropins.models.BaseAuth)
|
||||
|
||||
Returns:
|
||||
ndb.key.Key:
|
||||
"""
|
||||
match login.site_name():
|
||||
case 'Mastodon':
|
||||
if login.user_json and (id := json_loads(login.user_json).get('uri')):
|
||||
return ActivityPub(id=id).key
|
||||
logger.warning(f'Mastodon auth entity {login.key.id()} has no user_json or uri')
|
||||
return None
|
||||
case 'Pixelfed':
|
||||
user, server = login.key.id().strip('@').split('@')
|
||||
return ActivityPub(id=f'https://{server}/users/{user}').key
|
||||
case 'Bluesky':
|
||||
return ATProto(id=login.key.id()).key
|
||||
case _:
|
||||
assert False, repr(login)
|
||||
|
||||
|
||||
def render(template, **vars):
|
||||
"""Renders a Jinja2 template and adds our standard template variables.
|
||||
|
||||
|
@ -178,6 +230,14 @@ def login():
|
|||
)
|
||||
|
||||
|
||||
@app.post('/logout')
|
||||
def logout():
|
||||
"""Logs the user out of all current login sessions."""
|
||||
oauth_dropins.logout()
|
||||
flash(f"OK, you're now logged out.")
|
||||
return redirect('/', code=302)
|
||||
|
||||
|
||||
@app.route('/settings')
|
||||
@canonicalize_request_domain(common.PROTOCOL_DOMAINS, common.PRIMARY_DOMAIN)
|
||||
def settings():
|
||||
|
@ -187,31 +247,13 @@ def settings():
|
|||
|
||||
users = []
|
||||
user_keys = []
|
||||
|
||||
for login in get_logins():
|
||||
proto = key = None
|
||||
match login.site_name():
|
||||
case 'Mastodon':
|
||||
proto = ActivityPub
|
||||
if login.user_json and (id := json_loads(login.user_json).get('uri')):
|
||||
pass
|
||||
else:
|
||||
logger.warning(f'Mastodon auth entity {login.key.id()} has no user_json or uri')
|
||||
continue
|
||||
case 'Pixelfed':
|
||||
proto = ActivityPub
|
||||
user, server = login.key.id().strip('@').split('@')
|
||||
id = f'https://{server}/users/{user}'
|
||||
case 'Bluesky':
|
||||
proto = ATProto
|
||||
id = login.key.id()
|
||||
case _:
|
||||
assert False, repr(login)
|
||||
|
||||
if logged_in_as:
|
||||
users.append(proto.get_or_create(id, allow_opt_out=True))
|
||||
user_key = login_to_user_key(login)
|
||||
if user_key == logged_in_as:
|
||||
cls = Model._lookup_model(user_key.kind())
|
||||
users.append(cls.get_or_create(id=user_key.id(), allow_opt_out=True))
|
||||
else:
|
||||
user_keys.append(proto(id=id).key)
|
||||
user_keys.append(user_key)
|
||||
|
||||
users.extend(u for u in get_multi(user_keys) if u)
|
||||
if not users:
|
||||
|
@ -223,12 +265,61 @@ def settings():
|
|||
USER_STATUS_DESCRIPTIONS=USER_STATUS_DESCRIPTIONS,
|
||||
)
|
||||
|
||||
@app.post('/logout')
|
||||
def logout():
|
||||
"""Logs the user out of all current login sessions."""
|
||||
oauth_dropins.logout()
|
||||
flash(f"OK, you're now logged out.")
|
||||
return redirect('/', code=302)
|
||||
|
||||
@app.post('/settings/enable')
|
||||
@require_login
|
||||
def enable(user=None):
|
||||
"""Enables bridging for a given account.
|
||||
|
||||
Args:
|
||||
user (models.User)
|
||||
"""
|
||||
enabled = []
|
||||
|
||||
for proto in set(PROTOCOLS.values()):
|
||||
if (proto and not isinstance(user, proto)
|
||||
and proto.LABEL not in ('ui', 'web')
|
||||
and not user.is_enabled(proto)):
|
||||
try:
|
||||
user.enable_protocol(proto)
|
||||
except ErrorButDoNotRetryTask as e:
|
||||
msg = str(e)
|
||||
if resp := e.get_response():
|
||||
if resp.is_json:
|
||||
msg = resp.json['error']
|
||||
flash(f"Couldn't enable bridging to {proto.PHRASE}: {msg}")
|
||||
return redirect('/settings', code=302)
|
||||
|
||||
proto.bot_follow(user)
|
||||
enabled.append(proto)
|
||||
|
||||
if enabled:
|
||||
flash(f'Now bridging {user.handle_or_id()} to {",".join(p.PHRASE for p in enabled)}.')
|
||||
else:
|
||||
flash(f'{user.handle_or_id()} is already bridging.')
|
||||
|
||||
return redirect('/settings', code=302)
|
||||
|
||||
|
||||
@app.post('/settings/disable')
|
||||
@require_login
|
||||
def disable(user=None):
|
||||
"""Disables bridging for a given account.
|
||||
|
||||
Args:
|
||||
user (models.User)
|
||||
"""
|
||||
if not user.enabled_protocols:
|
||||
flash(f'{user.handle_or_id()} is not currently bridging.')
|
||||
return redirect('/settings', code=302)
|
||||
|
||||
enabled = list(user.enabled_protocols)
|
||||
for proto in user.enabled_protocols:
|
||||
user.delete(PROTOCOLS[proto])
|
||||
user.disable_protocol(PROTOCOLS[proto])
|
||||
|
||||
flash(f'Disabled bridging {user.handle_or_id()} to {",".join(PROTOCOLS[p].PHRASE for p in enabled)}.')
|
||||
return redirect('/settings', code=302)
|
||||
|
||||
|
||||
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>')
|
||||
|
|
|
@ -18,14 +18,15 @@
|
|||
{% if user.enabled_protocols and not user.status %}
|
||||
<a href="{{ user.user_page_path() }}">Currently bridging.</a>
|
||||
<!-- to {{ user.enabled_protocols|join(', ') }} -->
|
||||
<form method="post" action="#">
|
||||
<form method="post" action="/settings/disable"
|
||||
onsubmit="return window.confirm('Are you sure you want to disable bridging? Disabling has effects that can\'t easily be reversed, even if you later re-enable bridging. For example, disabling removes all fediverse followers.')" />
|
||||
<input name="key" type="hidden" value="{{ user.key.urlsafe().decode() }}" />
|
||||
<input type="submit" value="Disable" class="btn btn-default disable-button" />
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
Not bridging{% if user.status %} because {{ USER_STATUS_DESCRIPTIONS[user.status] }}{% endif %}.
|
||||
<form method="post" action="#">
|
||||
<form method="post" action="/settings/enable">
|
||||
<input name="key" type="hidden" value="{{ user.key.urlsafe().decode() }}" />
|
||||
<input type="submit" value="Enable" class="btn btn-default enable-button"
|
||||
{% if user.status %}disabled="true"{% endif %} />
|
||||
|
|
Ładowanie…
Reference in New Issue