bridgy-fed/pages.py

352 wiersze
11 KiB
Python
Czysty Zwykły widok Historia

"""UI pages."""
import datetime
import itertools
import logging
import os
noop, lint fixes from flake8 remaining: $ flake8 --extend-ignore=E501 *.py tests/*.py "pyflakes" failed during execution due to "'FlakesChecker' object has no attribute 'NAMEDEXPR'" Run flake8 with greater verbosity to see more details activitypub.py:15:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused activitypub.py:36:1: F401 'web' imported but unused activitypub.py:48:1: E302 expected 2 blank lines, found 1 activitypub.py:51:9: F811 redefinition of unused 'web' from line 36 app.py:6:1: F401 'flask_app.app' imported but unused app.py:9:1: F401 'activitypub' imported but unused app.py:9:1: F401 'convert' imported but unused app.py:9:1: F401 'follow' imported but unused app.py:9:1: F401 'pages' imported but unused app.py:9:1: F401 'redirect' imported but unused app.py:9:1: F401 'superfeedr' imported but unused app.py:9:1: F401 'ui' imported but unused app.py:9:1: F401 'webfinger' imported but unused app.py:9:1: F401 'web' imported but unused app.py:9:1: F401 'xrpc_actor' imported but unused app.py:9:1: F401 'xrpc_feed' imported but unused app.py:9:1: F401 'xrpc_graph' imported but unused app.py:9:19: E401 multiple imports on one line models.py:19:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused models.py:364:31: E114 indentation is not a multiple of four (comment) models.py:364:31: E116 unexpected indentation (comment) protocol.py:17:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused redirect.py:26:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused web.py:18:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:13:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:110:13: E122 continuation line missing indentation or outdented webfinger.py:111:13: E122 continuation line missing indentation or outdented webfinger.py:131:13: E122 continuation line missing indentation or outdented webfinger.py:132:13: E122 continuation line missing indentation or outdented webfinger.py:133:13: E122 continuation line missing indentation or outdented webfinger.py:134:13: E122 continuation line missing indentation or outdented tests/__init__.py:2:1: F401 'oauth_dropins.webutil.tests' imported but unused tests/test_follow.py:11:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_follow.py:14:1: F401 '.testutil.Fake' imported but unused tests/test_models.py:156:15: E122 continuation line missing indentation or outdented tests/test_models.py:157:15: E122 continuation line missing indentation or outdented tests/test_models.py:158:11: E122 continuation line missing indentation or outdented tests/test_web.py:12:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_web.py:17:1: F401 '.testutil' imported but unused tests/test_web.py:1513:13: E128 continuation line under-indented for visual indent tests/test_web.py:1514:9: E124 closing bracket does not match visual indentation tests/testutil.py:106:1: E402 module level import not at top of file tests/testutil.py:107:1: E402 module level import not at top of file tests/testutil.py:108:1: E402 module level import not at top of file tests/testutil.py:109:1: E402 module level import not at top of file tests/testutil.py:110:1: E402 module level import not at top of file tests/testutil.py:301:24: E203 whitespace before ':' tests/testutil.py:301:25: E701 multiple statements on one line (colon) tests/testutil.py:301:25: E231 missing whitespace after ':'
2023-06-20 18:22:54 +00:00
from flask import g, render_template, request
from google.cloud import ndb
from google.cloud.ndb.query import AND, OR
2022-11-09 15:53:00 +00:00
from google.cloud.ndb.stats import KindStat
from granary import as1, as2, atom, microformats2, rss
2022-12-02 22:46:18 +00:00
import humanize
from oauth_dropins.webutil import flask_util, logs, util
noop, lint fixes from flake8 remaining: $ flake8 --extend-ignore=E501 *.py tests/*.py "pyflakes" failed during execution due to "'FlakesChecker' object has no attribute 'NAMEDEXPR'" Run flake8 with greater verbosity to see more details activitypub.py:15:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused activitypub.py:36:1: F401 'web' imported but unused activitypub.py:48:1: E302 expected 2 blank lines, found 1 activitypub.py:51:9: F811 redefinition of unused 'web' from line 36 app.py:6:1: F401 'flask_app.app' imported but unused app.py:9:1: F401 'activitypub' imported but unused app.py:9:1: F401 'convert' imported but unused app.py:9:1: F401 'follow' imported but unused app.py:9:1: F401 'pages' imported but unused app.py:9:1: F401 'redirect' imported but unused app.py:9:1: F401 'superfeedr' imported but unused app.py:9:1: F401 'ui' imported but unused app.py:9:1: F401 'webfinger' imported but unused app.py:9:1: F401 'web' imported but unused app.py:9:1: F401 'xrpc_actor' imported but unused app.py:9:1: F401 'xrpc_feed' imported but unused app.py:9:1: F401 'xrpc_graph' imported but unused app.py:9:19: E401 multiple imports on one line models.py:19:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused models.py:364:31: E114 indentation is not a multiple of four (comment) models.py:364:31: E116 unexpected indentation (comment) protocol.py:17:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused redirect.py:26:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused web.py:18:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:13:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:110:13: E122 continuation line missing indentation or outdented webfinger.py:111:13: E122 continuation line missing indentation or outdented webfinger.py:131:13: E122 continuation line missing indentation or outdented webfinger.py:132:13: E122 continuation line missing indentation or outdented webfinger.py:133:13: E122 continuation line missing indentation or outdented webfinger.py:134:13: E122 continuation line missing indentation or outdented tests/__init__.py:2:1: F401 'oauth_dropins.webutil.tests' imported but unused tests/test_follow.py:11:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_follow.py:14:1: F401 '.testutil.Fake' imported but unused tests/test_models.py:156:15: E122 continuation line missing indentation or outdented tests/test_models.py:157:15: E122 continuation line missing indentation or outdented tests/test_models.py:158:11: E122 continuation line missing indentation or outdented tests/test_web.py:12:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_web.py:17:1: F401 '.testutil' imported but unused tests/test_web.py:1513:13: E128 continuation line under-indented for visual indent tests/test_web.py:1514:9: E124 closing bracket does not match visual indentation tests/testutil.py:106:1: E402 module level import not at top of file tests/testutil.py:107:1: E402 module level import not at top of file tests/testutil.py:108:1: E402 module level import not at top of file tests/testutil.py:109:1: E402 module level import not at top of file tests/testutil.py:110:1: E402 module level import not at top of file tests/testutil.py:301:24: E203 whitespace before ':' tests/testutil.py:301:25: E701 multiple statements on one line (colon) tests/testutil.py:301:25: E231 missing whitespace after ':'
2023-06-20 18:22:54 +00:00
from oauth_dropins.webutil.flask_util import error, redirect
import common
from common import DOMAIN_RE
from flask_app import app, cache
noop, lint fixes from flake8 remaining: $ flake8 --extend-ignore=E501 *.py tests/*.py "pyflakes" failed during execution due to "'FlakesChecker' object has no attribute 'NAMEDEXPR'" Run flake8 with greater verbosity to see more details activitypub.py:15:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused activitypub.py:36:1: F401 'web' imported but unused activitypub.py:48:1: E302 expected 2 blank lines, found 1 activitypub.py:51:9: F811 redefinition of unused 'web' from line 36 app.py:6:1: F401 'flask_app.app' imported but unused app.py:9:1: F401 'activitypub' imported but unused app.py:9:1: F401 'convert' imported but unused app.py:9:1: F401 'follow' imported but unused app.py:9:1: F401 'pages' imported but unused app.py:9:1: F401 'redirect' imported but unused app.py:9:1: F401 'superfeedr' imported but unused app.py:9:1: F401 'ui' imported but unused app.py:9:1: F401 'webfinger' imported but unused app.py:9:1: F401 'web' imported but unused app.py:9:1: F401 'xrpc_actor' imported but unused app.py:9:1: F401 'xrpc_feed' imported but unused app.py:9:1: F401 'xrpc_graph' imported but unused app.py:9:19: E401 multiple imports on one line models.py:19:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused models.py:364:31: E114 indentation is not a multiple of four (comment) models.py:364:31: E116 unexpected indentation (comment) protocol.py:17:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused redirect.py:26:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused web.py:18:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:13:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:110:13: E122 continuation line missing indentation or outdented webfinger.py:111:13: E122 continuation line missing indentation or outdented webfinger.py:131:13: E122 continuation line missing indentation or outdented webfinger.py:132:13: E122 continuation line missing indentation or outdented webfinger.py:133:13: E122 continuation line missing indentation or outdented webfinger.py:134:13: E122 continuation line missing indentation or outdented tests/__init__.py:2:1: F401 'oauth_dropins.webutil.tests' imported but unused tests/test_follow.py:11:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_follow.py:14:1: F401 '.testutil.Fake' imported but unused tests/test_models.py:156:15: E122 continuation line missing indentation or outdented tests/test_models.py:157:15: E122 continuation line missing indentation or outdented tests/test_models.py:158:11: E122 continuation line missing indentation or outdented tests/test_web.py:12:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_web.py:17:1: F401 '.testutil' imported but unused tests/test_web.py:1513:13: E128 continuation line under-indented for visual indent tests/test_web.py:1514:9: E124 closing bracket does not match visual indentation tests/testutil.py:106:1: E402 module level import not at top of file tests/testutil.py:107:1: E402 module level import not at top of file tests/testutil.py:108:1: E402 module level import not at top of file tests/testutil.py:109:1: E402 module level import not at top of file tests/testutil.py:110:1: E402 module level import not at top of file tests/testutil.py:301:24: E203 whitespace before ':' tests/testutil.py:301:25: E701 multiple statements on one line (colon) tests/testutil.py:301:25: E231 missing whitespace after ':'
2023-06-20 18:22:54 +00:00
from models import fetch_page, Follower, Object, PAGE_SIZE, PROTOCOLS
FOLLOWERS_UI_LIMIT = 999
# precompute this because we get a ton of requests for non-existing users
# from weird open redirect referrers:
# https://github.com/snarfed/bridgy-fed/issues/422
with app.test_request_context('/'):
USER_NOT_FOUND_HTML = render_template('user_not_found.html')
logger = logging.getLogger(__name__)
def load_user(protocol, id):
"""Loads the current request's user into `g.user`.
Args:
protocol: str
id: str
Raises:
:class:`werkzeug.exceptions.HTTPException` on error or redirect
"""
assert id
cls = PROTOCOLS[protocol]
g.user = cls.get_by_id(id)
if protocol != 'web':
if not g.user:
g.user = cls.query(cls.readable_id == id).get()
if g.user and g.user.use_instead:
g.user = g.user.use_instead.get()
if g.user and id != g.user.readable_or_key_id():
error('', status=302, location=g.user.user_page_path())
elif g.user and id != g.user.key.id(): # use_instead redirect
error('', status=302, location=g.user.user_page_path())
if not g.user or not g.user.direct:
# TODO: switch back to USER_NOT_FOUND_HTML
# not easy via exception/abort because this uses Werkzeug's built in
# NotFound exception subclass, and we'd need to make it implement
# get_body to return arbitrary HTML.
error(f'{protocol} user {id} not found', status=404)
assert not g.user.use_instead
@app.route('/')
@flask_util.cached(cache, datetime.timedelta(days=1))
def front_page():
"""View for the front page."""
return render_template('index.html')
@app.route('/docs')
@flask_util.cached(cache, datetime.timedelta(days=1))
def docs():
"""View for the docs page."""
return render_template('docs.html')
@app.get(f'/user/<regex("{DOMAIN_RE}"):domain>')
@app.get(f'/user/<regex("{DOMAIN_RE}"):domain>/feed')
@app.get(f'/user/<regex("{DOMAIN_RE}"):domain>/<any(followers,following):collection>')
def web_user_redirects(**kwargs):
path = request.url.removeprefix(request.root_url).removeprefix('user/')
return redirect(f'/web/{path}', code=301)
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>')
def user(protocol, id):
load_user(protocol, id)
query = Object.query(OR(Object.users == g.user.key,
Object.notify == g.user.key))
objects, before, after = fetch_objects(query)
followers = Follower.query(Follower.to == g.user.key,
Follower.status == 'active')\
.count(limit=FOLLOWERS_UI_LIMIT)
followers = f'{followers}{"+" if followers == FOLLOWERS_UI_LIMIT else ""}'
following = Follower.query(Follower.from_ == g.user.key,
Follower.status == 'active')\
.count(limit=FOLLOWERS_UI_LIMIT)
following = f'{following}{"+" if following == FOLLOWERS_UI_LIMIT else ""}'
return render_template(
'user.html',
follow_url=request.values.get('url'),
2022-11-13 07:19:09 +00:00
logs=logs,
util=util,
address=request.args.get('address'),
g=g,
**locals(),
)
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>/<any(followers,following):collection>')
def followers_or_following(protocol, id, collection):
load_user(protocol, id)
followers, before, after = Follower.fetch_page(collection)
return render_template(
f'{collection}.html',
address=request.args.get('address'),
as2=as2,
g=g,
util=util,
**locals()
)
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>/feed')
def feed(protocol, id):
format = request.args.get('format', 'html')
if format not in ('html', 'atom', 'rss'):
2022-11-17 15:58:08 +00:00
error(f'format {format} not supported; expected html, atom, or rss')
load_user(protocol, id)
objects = Object.query(OR(Object.feed == g.user.key,
# backward compatibility
AND(Object.users == g.user.key,
Object.labels == 'feed'))) \
.order(-Object.created) \
.fetch(PAGE_SIZE)
activities = [obj.as1 for obj in objects if not obj.deleted]
# fill in authors, actors, objects stored in their own Objects
fields = 'author', 'actor', 'object'
hydrate_ids = [id for id in itertools.chain(
*[[a[f] for f in fields if isinstance(a.get(f), str)]
for a in activities])]
if hydrate_ids:
keys = [ndb.Key(Object, id) for id in hydrate_ids]
hydrated = {o.key.id(): o.as1 for o in ndb.get_multi(keys) if o}
for a in activities:
for field in fields:
id = a.get(field)
if isinstance(id, str) and hydrated.get(id):
a[field] = hydrated[id]
actor = {
'displayName': id,
'url': g.user.web_url(),
}
title = f'Bridgy Fed feed for {id}'
# TODO: inject/merge common.pretty_link into microformats2.render_content
# (specifically into hcard_to_html) somehow to convert Mastodon URLs to @-@
# syntax. maybe a fediverse kwarg down through the call chain?
if format == 'html':
entries = [microformats2.object_to_html(a) for a in activities]
return render_template('feed.html', util=util, g=g, **locals())
elif format == 'atom':
body = atom.activities_to_atom(activities, actor=actor, title=title,
2022-11-17 15:58:08 +00:00
request_url=request.url)
return body, {'Content-Type': atom.CONTENT_TYPE}
elif format == 'rss':
body = rss.from_activities(activities, actor=actor, title=title,
2022-11-17 15:58:08 +00:00
feed_url=request.url)
return body, {'Content-Type': rss.CONTENT_TYPE}
def fetch_objects(query):
"""Fetches a page of Object entities from a datastore query.
2023-04-26 04:01:45 +00:00
Wraps :func:`models.fetch_page` and adds attributes to the returned Object
entities for rendering in objects.html.
Args:
query: :class:`ndb.Query`
Returns:
(results, new_before, new_after) tuple with:
results: list of Object entities
new_before, new_after: str query param values for `before` and `after`
to fetch the previous and next pages, respectively
"""
objects, new_before, new_after = fetch_page(query, Object)
# synthesize human-friendly content for objects
for i, obj in enumerate(objects):
obj_as1 = obj.as1
inner_obj = as1.get_object(obj_as1)
2022-11-21 05:35:55 +00:00
# synthesize text snippet
type = as1.object_type(obj_as1)
if type == 'post':
inner_type = inner_obj.get('objectType')
if inner_type:
type = inner_type
phrases = {
'article': 'posted',
'comment': 'replied',
2023-04-17 22:36:29 +00:00
'delete': 'deleted',
'follow': 'followed',
'invite': 'is invited to',
'issue': 'filed issue',
'like': 'liked',
'note': 'posted',
'post': 'posted',
'repost': 'reposted',
'rsvp-interested': 'is interested in',
'rsvp-maybe': 'might attend',
'rsvp-no': 'is not attending',
'rsvp-yes': 'is attending',
'share': 'reposted',
'stop-following': 'unfollowed',
'update': 'updated',
}
obj.phrase = phrases.get(type)
content = (inner_obj.get('content')
or inner_obj.get('displayName')
or inner_obj.get('summary'))
urls = as1.object_urls(inner_obj)
id = common.redirect_unwrap(inner_obj.get('id', ''))
url = urls[0] if urls else id
if (type == 'update' and
(obj.users and id.strip('/') == obj.users[0].id()
or obj.domains and id.strip('/') == f'https://{obj.domains[0]}')):
obj.phrase = 'updated'
obj_as1.update({
'content': 'their profile',
'url': id,
})
elif url:
content = common.pretty_link(url, text=content)
obj.content = (obj_as1.get('content')
or obj_as1.get('displayName')
or obj_as1.get('summary'))
obj.url = util.get_first(obj_as1, 'url')
if type in ('like', 'follow', 'repost', 'share') or not obj.content:
if obj.url:
obj.phrase = common.pretty_link(obj.url, text=obj.phrase,
attrs={'class': 'u-url'})
if content:
obj.content = content
obj.url = url
return objects, new_before, new_after
2022-11-09 15:53:00 +00:00
@app.get('/stats')
def stats():
2022-12-02 22:46:18 +00:00
def count(kind):
return humanize.intcomma(
KindStat.query(KindStat.kind_name == kind).get().count)
return render_template(
'stats.html',
users=count('MagicKey'),
objects=count('Object'),
2022-12-02 22:46:18 +00:00
followers=count('Follower'),
)
2022-11-09 15:53:00 +00:00
@app.get('/.well-known/nodeinfo')
@flask_util.cached(cache, datetime.timedelta(days=1))
def nodeinfo_jrd():
"""
https://nodeinfo.diaspora.software/protocol.html
"""
return {
'links': [{
'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.1',
'href': common.host_url('nodeinfo.json'),
}],
}, {
'Content-Type': 'application/jrd+json',
}
@app.get('/nodeinfo.json')
@flask_util.cached(cache, datetime.timedelta(days=1))
def nodeinfo():
"""
https://nodeinfo.diaspora.software/schema.html
"""
user_total = None
stat = KindStat.query(KindStat.kind_name == 'MagicKey').get()
if stat:
user_total = stat.count
return {
'version': '2.1',
'software': {
'name': 'bridgy-fed',
'version': os.getenv('GAE_VERSION'),
'repository': 'https://github.com/snarfed/bridgy-fed',
'web_url': 'https://fed.brid.gy/',
},
'protocols': [
'activitypub',
'bluesky',
'webmention',
],
'services': {
'outbound': [],
'inbound': [],
},
'usage': {
'users': {
'total': user_total,
# 'activeMonth':
# 'activeHalfyear':
},
'localPosts': Object.query(Object.source_protocol.IN(('web', 'webmention')),
Object.type.IN(['note', 'article']),
).count(),
'localComments': Object.query(Object.source_protocol.IN(('web', 'webmention')),
Object.type == 'comment',
).count(),
},
'openRegistrations': True,
'metadata': {},
}, {
# https://nodeinfo.diaspora.software/protocol.html
'Content-Type': 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"',
}
2021-07-13 15:06:35 +00:00
@app.get('/log')
@flask_util.cached(cache, logs.CACHE_TIME)
2021-07-13 15:06:35 +00:00
def log():
return logs.log()