move current user into Flask g request-global

pull/457/head
Ryan Barrett 2023-03-20 14:28:14 -07:00
rodzic b2c60226b7
commit fb5f7b3fb0
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
20 zmienionych plików z 258 dodań i 252 usunięć

Wyświetl plik

@ -6,7 +6,7 @@ import itertools
import logging
from urllib.parse import quote_plus
from flask import abort, request
from flask import abort, g, request
from granary import as1, as2
from httpsig import HeaderVerifier
from httpsig.requests_auth import HTTPSignatureAuth
@ -52,13 +52,13 @@ class ActivityPub(Protocol):
LABEL = 'activitypub'
@classmethod
def send(cls, obj, url, *, user=None, log_data=True):
def send(cls, obj, url, log_data=True):
"""Delivers an AS2 activity to an inbox URL."""
return signed_post(url, user=user, log_data=True, data=obj.as2)
return signed_post(url, log_data=True, data=obj.as2)
# TODO: return bool or otherwise unify return value with others
@classmethod
def fetch(cls, id, obj, *, user=None):
def fetch(cls, id, obj):
"""Tries to fetch an AS2 object and populate it into an :class:`Object`.
Uses HTTP content negotiation via the Content-Type header. If the url is
@ -79,7 +79,6 @@ class ActivityPub(Protocol):
Args:
id: str, object's URL id
obj: :class:`Object` to populate the fetched object into
user: optional :class:`User` we're fetching on behalf of
Raises:
:class:`requests.HTTPError`, :class:`werkzeug.exceptions.HTTPException`
@ -98,7 +97,7 @@ class ActivityPub(Protocol):
def _get(url, headers):
"""Returns None if we fetched and populated, resp otherwise."""
resp = signed_get(url, user=user, headers=headers, gateway=True)
resp = signed_get(url, headers=headers, gateway=True)
if not resp.content:
_error(resp, 'empty response')
elif common.content_type(resp) == as2.CONTENT_TYPE:
@ -128,12 +127,11 @@ class ActivityPub(Protocol):
_error(resp)
@classmethod
def verify_signature(cls, activity, *, user=None):
def verify_signature(cls, activity):
"""Verifies the current request's HTTP Signature.
Args:
activity: dict, AS2 activity
user: optional :class:`User`
Logs details of the result. Raises :class:`werkzeug.HTTPError` if the
signature is missing or invalid, otherwise does nothing and returns None.
@ -159,7 +157,7 @@ class ActivityPub(Protocol):
error('Invalid Digest header, required for HTTP Signature', status=401)
try:
key_actor = cls.get_object(keyId, user=user).as2
key_actor = cls.get_object(keyId).as2
except BadGateway:
obj_id = as1.get_object(activity).get('id')
if (activity.get('type') == 'Delete' and obj_id and
@ -185,23 +183,25 @@ class ActivityPub(Protocol):
error('HTTP Signature verification failed', status=401)
def signed_get(url, *, user=None, **kwargs):
return signed_request(util.requests_get, url, user=user, **kwargs)
def signed_get(url, **kwargs):
return signed_request(util.requests_get, url, **kwargs)
def signed_post(url, *, user=None, **kwargs):
assert user
return signed_request(util.requests_post, url, user=user, **kwargs)
def signed_post(url, **kwargs):
assert g.user
return signed_request(util.requests_post, url, **kwargs)
def signed_request(fn, url, *, user=None, data=None, log_data=True,
def signed_request(fn, url, data=None, log_data=True,
headers=None, **kwargs):
"""Wraps requests.* and adds HTTP Signature.
If the current session has a user (ie in g.user), signs with that user's
key. Otherwise, uses the default user snarfed.org.
Args:
fn: :func:`util.requests_get` or :func:`util.requests_get`
url: str
user: optional :class:`User` to sign request with
data: optional AS2 object
log_data: boolean, whether to log full data object
kwargs: passed through to requests
@ -212,8 +212,7 @@ def signed_request(fn, url, *, user=None, data=None, log_data=True,
headers = {}
# prepare HTTP Signature and headers
if not user:
user = default_signature_user()
user = g.user or default_signature_user()
if data:
if log_data:
@ -250,7 +249,7 @@ def signed_request(fn, url, *, user=None, data=None, log_data=True,
# handle GET redirects manually so that we generate a new HTTP signature
if resp.is_redirect and fn == util.requests_get:
return signed_request(fn, resp.headers['Location'], data=data, user=user,
return signed_request(fn, resp.headers['Location'], data=data,
headers=headers, log_data=log_data, **kwargs)
type = common.content_type(resp)
@ -261,24 +260,24 @@ def signed_request(fn, url, *, user=None, data=None, log_data=True,
return resp
def postprocess_as2(activity, *, user=None, target=None, create=True):
def postprocess_as2(activity, target=None, create=True):
"""Prepare an AS2 object to be served or sent via ActivityPub.
g.user is required. Populates it into the actor.id and publicKey fields.
Args:
activity: dict, AS2 object or activity
user: :class:`User`, required. populated into actor.id and
publicKey fields if needed.
target: dict, AS2 object, optional. The target of activity's inReplyTo or
Like/Announce/etc object, if any.
create: boolean, whether to wrap `Note` and `Article` objects in a
`Create` activity
"""
assert user
assert g.user
type = activity.get('type')
# actor objects
if type == 'Person':
postprocess_as2_actor(activity, user=user)
postprocess_as2_actor(activity)
if not activity.get('publicKey'):
# underspecified, inferred from this issue and Mastodon's implementation:
# https://github.com/w3c/activitypub/issues/203#issuecomment-297553229
@ -288,7 +287,7 @@ def postprocess_as2(activity, *, user=None, target=None, create=True):
'publicKey': {
'id': actor_url,
'owner': actor_url,
'publicKeyPem': user.public_pem().decode(),
'publicKeyPem': g.user.public_pem().decode(),
},
'@context': (util.get_list(activity, '@context') +
['https://w3id.org/security/v1']),
@ -296,7 +295,7 @@ def postprocess_as2(activity, *, user=None, target=None, create=True):
return activity
for field in 'actor', 'attributedTo':
activity[field] = [postprocess_as2_actor(actor, user=user)
activity[field] = [postprocess_as2_actor(actor)
for actor in util.get_list(activity, field)]
if len(activity[field]) == 1:
activity[field] = activity[field][0]
@ -402,29 +401,28 @@ def postprocess_as2(activity, *, user=None, target=None, create=True):
'@context': as2.CONTEXT,
'type': 'Create',
'id': f'{activity["id"]}#bridgy-fed-create',
'actor': postprocess_as2_actor({}, user=user),
'actor': postprocess_as2_actor({}),
'object': activity,
}
return util.trim_nulls(activity)
def postprocess_as2_actor(actor, *, user=None):
def postprocess_as2_actor(actor):
"""Prepare an AS2 actor object to be served or sent via ActivityPub.
Modifies actor in place.
Args:
actor: dict, AS2 actor object
user: :class:`User`
Returns:
actor dict
"""
if not actor or isinstance(actor, str):
return user.actor_id() if user.is_homepage(actor) else actor
return g.user.actor_id() if g.user.is_homepage(actor) else actor
url = user.homepage if user else None
url = g.user.homepage if g.user else None
urls = util.get_list(actor, 'url')
if not urls and url:
urls = [url]
@ -461,16 +459,16 @@ def actor(domain):
if tld in TLD_BLOCKLIST:
error('', status=404)
user = User.get_by_id(domain)
if not user:
g.user = User.get_by_id(domain)
if not g.user:
return f'User {domain} not found', 404
elif not user.actor_as2:
elif not g.user.actor_as2:
return f'User {domain} not fully set up', 404
# TODO: unify with common.actor()
actor = postprocess_as2(user.actor_as2, user=user)
actor = postprocess_as2(g.user.actor_as2)
actor.update({
'id': user.actor_id(),
'id': g.user.actor_id(),
# This has to be the domain for Mastodon etc interop! It seems like it
# should be the custom username from the acct: u-url in their h-card,
# but that breaks Mastodon's Webfinger discovery. Background:
@ -512,14 +510,12 @@ def inbox(domain=None):
logger.info(f'Got {type} from {actor_id}: {json_dumps(activity, indent=2)}')
# load user
# TODO: store in g instead of passing around
user = None
if domain:
user = User.get_by_id(domain)
if not user:
g.user = User.get_by_id(domain)
if not g.user:
error(f'User {domain} not found', status=404)
ActivityPub.verify_signature(activity, user=user)
ActivityPub.verify_signature(activity)
# check that this activity is public. only do this for creates, not likes,
# follows, or other activity types, since Mastodon doesn't currently mark
@ -541,7 +537,7 @@ def inbox(domain=None):
followee_url = redirect_unwrap(util.get_url(activity, 'object'))
activity.setdefault('url', f'{follower_url}#followed-{followee_url}')
return ActivityPub.receive(activity.get('id'), user=user,
return ActivityPub.receive(activity.get('id'),
as2=redirect_unwrap(activity))

6
app.py
Wyświetl plik

@ -3,7 +3,7 @@ import json
import logging
from pathlib import Path
from flask import Flask
from flask import Flask, g
from flask_caching import Cache
import flask_gae_static
from lexrpc.server import Server
@ -34,6 +34,10 @@ app.register_error_handler(Exception, flask_util.handle_exception)
if appengine_info.LOCAL:
flask_gae_static.init_app(app)
@app.before_request
def init_globals():
g.user = None
# don't redirect API requests with blank path elements
app.url_map.redirect_defaults = True

Wyświetl plik

@ -9,7 +9,7 @@ import threading
import urllib.parse
import cachetools
from flask import abort, make_response, request
from flask import abort, g, make_response, request
from granary import as1, as2, microformats2
import mf2util
from oauth_dropins.webutil import util, webmention
@ -64,7 +64,7 @@ def error(msg, status=400):
abort(status, response=make_response({'error': msg}, status))
def pretty_link(url, text=None, user=None, **kwargs):
def pretty_link(url, text=None, **kwargs):
"""Wrapper around util.pretty_link() that converts Mastodon user URLs to @-@.
Eg for URLs like https://mastodon.social/@foo and
@ -74,11 +74,10 @@ def pretty_link(url, text=None, user=None, **kwargs):
Args:
url: str
text: str
user: :class:`User`, optional, user for the current request
kwargs: passed through to :func:`webutil.util.pretty_link`
"""
if user and user.is_homepage(url):
return user.user_page_link()
if g.user and g.user.is_homepage(url):
return g.user.user_page_link()
if text is None:
match = re.match(r'https?://([^/]+)/(@|users/)([^/]+)$', url)
@ -218,7 +217,7 @@ def actor(user):
actor_as1 = microformats2.json_to_object(hcard, rel_urls=mf2.get('rel-urls'))
# TODO: fix circular dependency
import activitypub
actor_as2 = activitypub.postprocess_as2(as2.from_as1(actor_as1), user=user)
actor_as2 = activitypub.postprocess_as2(as2.from_as1(actor_as1))
# TODO: unify with activitypub.actor()
actor_as2.update({
'id': user.actor_id(),

Wyświetl plik

@ -7,7 +7,7 @@ https://www.rfc-editor.org/rfc/rfc7033
import logging
import urllib.parse
from flask import redirect, request, session
from flask import g, redirect, request, session
from granary import as2
from oauth_dropins import indieauth
from oauth_dropins.webutil import flask_util, util
@ -81,8 +81,8 @@ def remote_follow():
logger.info(f'Got: {request.values}')
domain = request.values['domain']
user = User.get_by_id(domain)
if not user:
g.user = User.get_by_id(domain)
if not g.user:
error(f'No Bridgy Fed user found for domain {domain}')
addr = request.values['address']
@ -94,7 +94,7 @@ def remote_follow():
if link.get('rel') == SUBSCRIBE_LINK_REL:
template = link.get('template')
if template and '{uri}' in template:
return redirect(template.replace('{uri}', user.address()))
return redirect(template.replace('{uri}', g.user.address()))
flash(f"Couldn't find remote follow link for {addr}")
return redirect(f'/user/{domain}')
@ -134,10 +134,10 @@ class FollowCallback(indieauth.Callback):
session['indieauthed-me'] = me
domain = util.domain_from_link(me)
user = User.get_by_id(domain)
if not user:
g.user = User.get_by_id(domain)
if not g.user:
error(f'No user for domain {domain}')
domain = user.key.id()
domain = g.user.key.id()
addr = state
if not state:
@ -159,7 +159,7 @@ class FollowCallback(indieauth.Callback):
return redirect(f'/user/{domain}/following')
# TODO: make this generic across protocols
followee = activitypub.ActivityPub.get_object(as2_url, user=user).as2
followee = activitypub.ActivityPub.get_object(as2_url).as2
id = followee.get('id')
inbox = followee.get('inbox')
if not id or not inbox:
@ -173,12 +173,12 @@ class FollowCallback(indieauth.Callback):
'type': 'Follow',
'id': follow_id,
'object': followee,
'actor': user.actor_id(),
'actor': g.user.actor_id(),
'to': [as2.PUBLIC_AUDIENCE],
}
obj = Object(id=follow_id, domains=[domain], labels=['user', 'activity'],
source_protocol='ui', status='complete', as2=follow_as2)
activitypub.ActivityPub.send(obj, inbox, user=user)
activitypub.ActivityPub.send(obj, inbox)
Follower.get_or_create(dest=id, src=domain, status='active',
last_follow=follow_as2)
@ -221,10 +221,10 @@ class UnfollowCallback(indieauth.Callback):
session['indieauthed-me'] = me
domain = util.domain_from_link(me)
user = User.get_by_id(domain)
if not user:
g.user = User.get_by_id(domain)
if not g.user:
error(f'No user for domain {domain}')
domain = user.key.id()
domain = g.user.key.id()
follower = Follower.get_by_id(state)
if not follower:
@ -237,7 +237,7 @@ class UnfollowCallback(indieauth.Callback):
if isinstance(followee, str):
# fetch as AS2 to get full followee with inbox
followee_id = followee
followee = activitypub.ActivityPub.get_object(followee_id, user=user).as2
followee = activitypub.ActivityPub.get_object(followee_id).as2
inbox = followee.get('inbox')
if not inbox:
@ -250,13 +250,13 @@ class UnfollowCallback(indieauth.Callback):
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Undo',
'id': unfollow_id,
'actor': user.actor_id(),
'actor': g.user.actor_id(),
'object': follower.last_follow,
}
obj = Object(id=unfollow_id, domains=[domain], labels=['user', 'activity'],
source_protocol='ui', status='complete', as2=unfollow_as2)
activitypub.ActivityPub.send(obj, inbox, user=user)
activitypub.ActivityPub.send(obj, inbox)
follower.status = 'inactive'
follower.put()

Wyświetl plik

@ -12,7 +12,7 @@ from werkzeug.exceptions import BadRequest, NotFound
from Crypto import Random
from Crypto.PublicKey import RSA
from Crypto.Util import number
from flask import request
from flask import g, request
from google.cloud import ndb
from granary import as1, as2, bluesky, microformats2
from oauth_dropins.webutil.appengine_info import DEBUG
@ -351,30 +351,26 @@ class Object(StringIdModel):
return common.host_url('render?' +
urllib.parse.urlencode({'id': self.key.id()}))
def actor_link(self, user=None):
"""Returns a pretty actor link with their name and profile picture.
Args:
cur_user: :class:`User`, optional, user for the current request
"""
def actor_link(self):
"""Returns a pretty actor link with their name and profile picture."""
attrs = {'class': 'h-card u-author'}
if (self.source_protocol in ('webmention', 'ui') and user and
user.key.id() in self.domains):
if (self.source_protocol in ('webmention', 'ui') and g.user and
g.user.key.id() in self.domains):
# outbound; show a nice link to the user
return user.user_page_link()
return g.user.user_page_link()
actor = (util.get_first(self.as1, 'actor')
or util.get_first(self.as1, 'author')
or {})
if isinstance(actor, str):
return common.pretty_link(actor, user=user, attrs=attrs)
return common.pretty_link(actor, attrs=attrs)
url = util.get_first(actor, 'url') or ''
name = actor.get('displayName') or actor.get('username') or ''
image = util.get_url(actor, 'image')
if not image:
return common.pretty_link(url, text=name, user=user, attrs=attrs)
return common.pretty_link(url, text=name, attrs=attrs)
return f"""\
<a class="h-card u-author" href="{url}" title="{name}">

Wyświetl plik

@ -6,7 +6,7 @@ import os
import re
import urllib.parse
from flask import redirect, render_template, request
from flask import g, redirect, render_template, request
from google.cloud.ndb.stats import KindStat
from granary import as1, as2, atom, microformats2, rss
import humanize
@ -58,9 +58,9 @@ def check_web_site():
flash(f'No domain found in {url}')
return render_template('enter_web_site.html')
user = User.get_or_create(domain)
g.user = User.get_or_create(domain)
try:
user = user.verify()
g.user = g.user.verify()
except BaseException as e:
code, body = util.interpret_http_exception(e)
if code:
@ -68,25 +68,25 @@ def check_web_site():
return render_template('enter_web_site.html')
raise
user.put()
return redirect(f'/user/{user.key.id()}')
g.user.put()
return redirect(f'/user/{g.user.key.id()}')
@app.get(f'/user/<regex("{DOMAIN_RE}"):domain>')
def user(domain):
user = User.get_by_id(domain)
if not user:
g.user = User.get_by_id(domain)
if not g.user:
return USER_NOT_FOUND_HTML, 404
elif user.key.id() != domain:
return redirect(f'/user/{user.key.id()}', code=301)
elif g.user.key.id() != domain:
return redirect(f'/user/{g.user.key.id()}', code=301)
assert not user.use_instead
assert not g.user.use_instead
query = Object.query(
Object.domains == domain,
Object.labels.IN(('notification', 'user')),
)
objects, before, after = fetch_objects(query, user)
objects, before, after = fetch_objects(query)
followers = Follower.query(Follower.dest == domain, Follower.status == 'active')\
.count(limit=FOLLOWERS_UI_LIMIT)
@ -102,13 +102,15 @@ def user(domain):
logs=logs,
util=util,
address=request.args.get('address'),
g=g,
**locals(),
)
@app.get(f'/user/<regex("{DOMAIN_RE}"):domain>/<any(followers,following):collection>')
def followers_or_following(domain, collection):
if not (user := User.get_by_id(domain)): # user var is used in template
g.user = User.get_by_id(domain) # g.user is used in template
if not g.user:
return USER_NOT_FOUND_HTML, 404
followers, before, after = Follower.fetch_page(domain, collection)
@ -125,6 +127,7 @@ def followers_or_following(domain, collection):
f'{collection}.html',
util=util,
address=request.args.get('address'),
g=g,
**locals()
)
@ -135,8 +138,9 @@ def feed(domain):
if format not in ('html', 'atom', 'rss'):
error(f'format {format} not supported; expected html, atom, or rss')
if not (user := User.get_by_id(domain)):
return render_template('user_not_found.html', domain=domain), 404
g.user = User.get_by_id(domain)
if not g.user:
return render_template('user_not_found.html', domain=domain), 404
objects, _, _ = Object.query(
Object.domains == domain, Object.labels == 'feed') \
@ -146,7 +150,7 @@ def feed(domain):
actor = {
'displayName': domain,
'url': user.homepage,
'url': g.user.homepage,
}
title = f'Bridgy Fed feed for {domain}'
@ -155,7 +159,7 @@ def feed(domain):
# 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, **locals())
return render_template('feed.html', util=util, g=g, **locals())
elif format == 'atom':
body = atom.activities_to_atom(activities, actor=actor, title=title,
request_url=request.url)
@ -166,7 +170,7 @@ def feed(domain):
return body, {'Content-Type': rss.CONTENT_TYPE}
def fetch_objects(query, user):
def fetch_objects(query):
"""Fetches a page of Object entities from a datastore query.
Wraps :func:`common.fetch_page` and adds attributes to the returned Object
@ -174,7 +178,6 @@ def fetch_objects(query, user):
Args:
query: :class:`ndb.Query`
user: :class:`User`
Returns:
(results, new_before, new_after) tuple with:
@ -228,7 +231,7 @@ def fetch_objects(query, user):
'url': f'https://{obj.domains[0]}',
})
elif url:
content = common.pretty_link(url, text=content, user=user)
content = common.pretty_link(url, text=content)
obj.content = (obj.as1.get('content')
or obj.as1.get('displayName')
@ -238,7 +241,7 @@ def fetch_objects(query, user):
if (type in ('like', 'follow', 'repost', 'share') or
not obj.content):
if obj.url:
obj.phrase = common.pretty_link(obj.url, text=obj.phrase, user=user,
obj.phrase = common.pretty_link(obj.url, text=obj.phrase,
attrs={'class': 'u-url'})
if content:
obj.content = content

Wyświetl plik

@ -3,6 +3,7 @@ import logging
import threading
from cachetools import cached, LRUCache
from flask import g
from google.cloud import ndb
from google.cloud.ndb import OR
from granary import as1, as2
@ -53,7 +54,7 @@ class Protocol:
assert False
@classmethod
def send(cls, obj, url, *, user=None, log_data=True):
def send(cls, obj, url, log_data=True):
"""Sends an outgoing activity.
To be implemented by subclasses.
@ -61,7 +62,6 @@ class Protocol:
Args:
obj: :class:`Object` with activity to send
url: str, destination URL to send to
user: :class:`User` this is on behalf of
log_data: boolean, whether to log full data object
Returns:
@ -74,7 +74,7 @@ class Protocol:
raise NotImplementedError()
@classmethod
def fetch(cls, obj, id, *, user=None):
def fetch(cls, obj, id):
"""Fetches a protocol-specific object and populates it into an :class:`Object`.
To be implemented by subclasses.
@ -82,7 +82,6 @@ class Protocol:
Args:
obj: :class:`Object` to populate the fetched object into
id: str, object's URL id
user: optional :class:`User` we're fetching on behalf of
Raises:
:class:`werkzeug.HTTPException` if the fetch fails
@ -90,12 +89,11 @@ class Protocol:
raise NotImplementedError()
@classmethod
def receive(cls, id, *, user=None, **props):
def receive(cls, id, **props):
"""Handles an incoming activity.
Args:
id: str, activity id
user: :class:`User`, optional receiving Bridgy Fed user
props: property values to populate into the :class:`Object`
Returns:
@ -195,18 +193,18 @@ class Protocol:
# fetch actor if necessary so we have name, profile photo, etc
if actor and actor.keys() == set(['id']):
actor = obj.as2['actor'] = cls.get_object(actor['id'], user=user).as2
actor = obj.as2['actor'] = cls.get_object(actor['id']).as2
# fetch object if necessary so we can render it in feeds
if obj.type == 'share' and inner_obj.keys() == set(['id']):
inner_obj = obj.as2['object'] = as2.from_as1(
cls.get_object(inner_obj_id, user=user).as1)
cls.get_object(inner_obj_id).as1)
if obj.type == 'follow':
cls.accept_follow(obj, user=user)
cls.accept_follow(obj)
# send webmentions to each target
send_webmentions(obj, user=user, proxy=True)
send_webmentions(obj, proxy=True)
# deliver original posts and reposts to followers
is_reply = (obj.type == 'comment' or
@ -229,14 +227,13 @@ class Protocol:
return 'OK'
@classmethod
def accept_follow(cls, obj, *, user=None):
def accept_follow(cls, obj):
"""Replies to an AP Follow request with an Accept request.
TODO: move to Protocol
Args:
obj: :class:`Object`
user: :class:`User`
"""
logger.info('Replying to Follow with Accept')
@ -253,16 +250,16 @@ class Protocol:
# store Follower
follower_obj = models.Follower.get_or_create(
dest=user.key.id(), src=follower_id, last_follow=obj.as2)
dest=g.user.key.id(), src=follower_id, last_follow=obj.as2)
follower_obj.status = 'active'
follower_obj.put()
# send AP Accept
followee_actor_url = user.actor_id()
followee_actor_url = g.user.actor_id()
accept = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': util.tag_uri(common.PRIMARY_DOMAIN,
f'accept/{user.key.id()}/{obj.key.id()}'),
f'accept/{g.user.key.id()}/{obj.key.id()}'),
'type': 'Accept',
'actor': followee_actor_url,
'object': {
@ -272,12 +269,12 @@ class Protocol:
}
}
return cls.send(models.Object(as2=accept), inbox, user=user)
return cls.send(models.Object(as2=accept), inbox)
@classmethod
@cached(LRUCache(1000), key=lambda cls, id, user=None: util.fragmentless(id),
@cached(LRUCache(1000), key=lambda cls, id: util.fragmentless(id),
lock=threading.Lock())
def get_object(cls, id, *, user=None):
def get_object(cls, id):
"""Loads and returns an Object from memory cache, datastore, or HTTP fetch.
Assumes id is a URL. Any fragment at the end is stripped before loading.
@ -294,7 +291,6 @@ class Protocol:
Args:
id: str
user: optional, :class:`User` used to sign HTTP request, if necessary
Returns: Object
@ -313,18 +309,17 @@ class Protocol:
logger.info(f'Object not in datastore or has no data: {id}')
obj.clear()
cls.fetch(id, obj, user=user)
cls.fetch(id, obj)
obj.source_protocol = cls.LABEL
obj.put()
return obj
def send_webmentions(obj, *, user=None, proxy=None):
def send_webmentions(obj, proxy=None):
"""Sends webmentions for an incoming ActivityPub inbox delivery.
Args:
obj: :class:`Object`
user: :class:`User`
proxy: boolean, whether to use our proxy URL as the webmention source
Returns: boolean, True if any webmentions were sent, False otherwise
@ -372,7 +367,7 @@ def send_webmentions(obj, *, user=None, proxy=None):
while obj.undelivered:
target = obj.undelivered.pop()
domain = util.domain_from_link(target.uri, minimize=False)
if user and domain == user.key.id():
if g.user and domain == g.user.key.id():
if 'notification' not in obj.labels:
obj.labels.append('notification')

Wyświetl plik

@ -14,7 +14,7 @@ import logging
import re
import urllib.parse
from flask import redirect, request
from flask import g, redirect, request
from granary import as2
from negotiator import ContentNegotiator, AcceptParameters, ContentType
from oauth_dropins.webutil import flask_util, util
@ -58,8 +58,8 @@ def redir(to):
urllib.parse.urlparse(to).hostname))
for domain in domains:
if domain:
user = User.get_by_id(domain)
if user:
g.user = User.get_by_id(domain)
if g.user:
logger.info(f'Found User for domain {domain}')
break
else:
@ -80,8 +80,7 @@ def redir(to):
obj = Object.get_by_id(to)
if not obj or obj.deleted:
return f'Object not found: {to}', 404
ret = activitypub.postprocess_as2(as2.from_as1(obj.as1),
user=user, create=False)
ret = activitypub.postprocess_as2(as2.from_as1(obj.as1), create=False)
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
return ret, {
'Content-Type': type,

Wyświetl plik

@ -4,7 +4,7 @@
{% for obj in objects %}
<li class="row h-entry">
<div class="e-content col-sm-{{ 5 if show_domains else 8 }}">
{{ obj.actor_link(user=user)|safe }}
{{ obj.actor_link()|safe }}
{{ obj.phrase|safe }}
<a target="_blank" href="{{ obj.url }}" class="u-url">
{{ obj.content|default('--', true)|striptags|truncate(50) }}

Wyświetl plik

@ -4,7 +4,7 @@
{% block content %}
{% if user.has_redirects == False %}
{% if g.user.has_redirects == False %}
<div class="row promo warning">
<form method="post" action="/web-site">
Next step:
@ -15,15 +15,15 @@
<input type="submit" class="btn btn-default" value="Check now" />
</form>
{% if user.redirects_error %}
{% if g.user.redirects_error %}
<details class="small">
{{ user.redirects_error|safe }}
{{ g.user.redirects_error|safe }}
</details>
{% endif %}
</div>
{% endif %}
{% if user.has_hcard == False %}
{% if g.user.has_hcard == False %}
<div class="row promo warning">
<form method="post" action="/web-site">
Next step:

Wyświetl plik

@ -1,11 +1,11 @@
<div class="row">
<div class="big" style="display: inline">
{{ user.user_page_link()|safe }}
{{ g.user.user_page_link()|safe }}
&middot;
<span title="Fediverse address">
<nobr>
<img class="logo" src="/static/fediverse_logo.svg">
{{ user.address() }}
{{ g.user.address() }}
</nobr>
</span>
&middot;

Wyświetl plik

@ -8,6 +8,7 @@ import logging
from unittest.mock import ANY, call, patch
import urllib.parse
from flask import g
from google.cloud import ndb
from granary import as2, microformats2
from httpsig import HeaderSigner
@ -1160,9 +1161,9 @@ class ActivityPubTest(testutil.TestCase):
class ActivityPubUtilsTest(testutil.TestCase):
def setUp(self):
super().setUp()
self.user = self.make_user('user.com', has_hcard=True, actor_as2=ACTOR)
self.app_context = app.test_request_context('/')
self.app_context.__enter__()
g.user = self.make_user('user.com', has_hcard=True, actor_as2=ACTOR)
def tearDown(self):
self.app_context.__exit__(None, None, None)
@ -1176,7 +1177,7 @@ class ActivityPubUtilsTest(testutil.TestCase):
}, activitypub.postprocess_as2({
'id': 'xyz',
'inReplyTo': ['foo', 'bar'],
}, user=User(id='site')))
}))
def test_postprocess_as2_multiple_url(self):
self.assert_equals({
@ -1186,7 +1187,7 @@ class ActivityPubUtilsTest(testutil.TestCase):
}, activitypub.postprocess_as2({
'id': 'xyz',
'url': ['foo', 'bar'],
}, user=User(id='site')))
}))
def test_postprocess_as2_multiple_image(self):
self.assert_equals({
@ -1197,9 +1198,10 @@ class ActivityPubUtilsTest(testutil.TestCase):
}, activitypub.postprocess_as2({
'id': 'xyz',
'image': [{'url': 'http://r/foo'}, {'url': 'http://r/bar'}],
}, user=User(id='site')))
}))
def test_postprocess_as2_actor_attributedTo(self):
g.user = User(id='site')
self.assert_equals({
'actor': {
'id': 'baj',
@ -1219,7 +1221,7 @@ class ActivityPubUtilsTest(testutil.TestCase):
}, activitypub.postprocess_as2({
'attributedTo': [{'id': 'bar'}, {'id': 'baz'}],
'actor': {'id': 'baj'},
}, user=User(id='site')))
}))
def test_postprocess_as2_note(self):
self.assert_equals({
@ -1235,7 +1237,7 @@ class ActivityPubUtilsTest(testutil.TestCase):
}, activitypub.postprocess_as2({
'id': 'xyz',
'type': 'Note',
}, user=User(id='site')))
}))
def test_postprocess_as2_hashtag(self):
"""https://github.com/snarfed/bridgy-fed/issues/45"""
@ -1253,7 +1255,7 @@ class ActivityPubUtilsTest(testutil.TestCase):
# should leave alone
{'type': 'Mention', 'href': 'foo'},
],
}, user=User(id='site')))
}))
# TODO: make these generic and use FakeProtocol
@patch('requests.get')
@ -1334,7 +1336,7 @@ class ActivityPubUtilsTest(testutil.TestCase):
allow_redirects=False),
requests_response(status=200, allow_redirects=False),
]
resp = activitypub.signed_get('https://first', user=self.user)
resp = activitypub.signed_get('https://first')
first = mock_get.call_args_list[0][1]
second = mock_get.call_args_list[1][1]
@ -1350,7 +1352,7 @@ class ActivityPubUtilsTest(testutil.TestCase):
allow_redirects=False),
]
resp = activitypub.signed_post('https://first', user=self.user)
resp = activitypub.signed_post('https://first')
mock_post.assert_called_once()
self.assertEqual(302, resp.status_code)
@ -1359,7 +1361,7 @@ class ActivityPubUtilsTest(testutil.TestCase):
mock_get.return_value = AS2
obj = Object()
ActivityPub.fetch('http://orig', obj, user=self.user)
ActivityPub.fetch('http://orig', obj)
self.assertEqual(AS2_OBJ, obj.as2)
mock_get.assert_has_calls((
self.as2_req('http://orig'),
@ -1370,7 +1372,7 @@ class ActivityPubUtilsTest(testutil.TestCase):
mock_get.side_effect = [HTML_WITH_AS2, AS2]
obj = Object()
ActivityPub.fetch('http://orig', obj, user=self.user)
ActivityPub.fetch('http://orig', obj)
self.assertEqual(AS2_OBJ, obj.as2)
mock_get.assert_has_calls((
self.as2_req('http://orig'),
@ -1381,19 +1383,19 @@ class ActivityPubUtilsTest(testutil.TestCase):
def test_fetch_only_html(self, mock_get):
mock_get.return_value = HTML
with self.assertRaises(BadGateway):
ActivityPub.fetch('http://orig', Object(), user=self.user)
ActivityPub.fetch('http://orig', Object())
@patch('requests.get')
def test_fetch_not_acceptable(self, mock_get):
mock_get.return_value=NOT_ACCEPTABLE
with self.assertRaises(BadGateway):
ActivityPub.fetch('http://orig', Object(), user=self.user)
ActivityPub.fetch('http://orig', Object())
@patch('requests.get')
def test_fetch_ssl_error(self, mock_get):
mock_get.side_effect = requests.exceptions.SSLError
with self.assertRaises(BadGateway):
ActivityPub.fetch('http://orig', Object(), user=self.user)
ActivityPub.fetch('http://orig', Object())
@patch('requests.get')
def test_fetch_datastore_no_content(self, mock_get):

Wyświetl plik

@ -1,6 +1,7 @@
"""Unit tests for common.py."""
from unittest import mock
from flask import g
from granary import as2
from oauth_dropins.webutil import appengine_config, util
from oauth_dropins.webutil.testutil import requests_response
@ -24,6 +25,7 @@ class CommonTest(testutil.TestCase):
super().setUp()
self.app_context = app.test_request_context('/')
self.app_context.push()
g.user = User(id='site')
def tearDown(self):
self.app_context.pop()
@ -46,7 +48,7 @@ class CommonTest(testutil.TestCase):
self.assertEqual(
'<a class="h-card u-author" href="/user/site"><img src="" class="profile"> site</a>',
common.pretty_link('https://site/', user=self.user))
common.pretty_link('https://site/'))
def test_redirect_wrap_empty(self):
self.assertIsNone(common.redirect_wrap(None))

Wyświetl plik

@ -2,7 +2,7 @@
"""Unit tests for models.py."""
from unittest import mock
from flask import get_flashed_messages
from flask import g, get_flashed_messages
from granary import as2
from oauth_dropins.webutil.testutil import requests_response
@ -17,76 +17,80 @@ from .test_activitypub import ACTOR
class UserTest(testutil.TestCase):
def setUp(self):
super(UserTest, self).setUp()
self.user = self.make_user('y.z')
super().setUp()
self.app_context = app.test_request_context('/')
self.app_context.push()
g.user = self.make_user('y.z')
self.full_redir = requests_response(
status=302,
redirected_url='http://localhost/.well-known/webfinger?resource=acct:y.z@y.z')
def tearDown(self):
self.app_context.pop()
super().tearDown()
def test_get_or_create(self):
assert self.user.mod
assert self.user.public_exponent
assert self.user.private_exponent
assert g.user.mod
assert g.user.public_exponent
assert g.user.private_exponent
same = User.get_or_create('y.z')
self.assertEqual(same, self.user)
self.assertEqual(same, g.user)
def test_get_or_create_use_instead(self):
user = User.get_or_create('a.b')
user.use_instead = self.user.key
user.use_instead = g.user.key
user.put()
self.assertEqual('y.z', User.get_or_create('a.b').key.id())
def test_href(self):
href = self.user.href()
href = g.user.href()
self.assertTrue(href.startswith('data:application/magic-public-key,RSA.'), href)
self.assertIn(self.user.mod, href)
self.assertIn(self.user.public_exponent, href)
self.assertIn(g.user.mod, href)
self.assertIn(g.user.public_exponent, href)
def test_public_pem(self):
pem = self.user.public_pem()
pem = g.user.public_pem()
self.assertTrue(pem.decode().startswith('-----BEGIN PUBLIC KEY-----\n'), pem)
self.assertTrue(pem.decode().endswith('-----END PUBLIC KEY-----'), pem)
def test_private_pem(self):
pem = self.user.private_pem()
pem = g.user.private_pem()
self.assertTrue(pem.decode().startswith('-----BEGIN RSA PRIVATE KEY-----\n'), pem)
self.assertTrue(pem.decode().endswith('-----END RSA PRIVATE KEY-----'), pem)
def test_address(self):
self.assertEqual('@y.z@y.z', self.user.address())
self.assertEqual('@y.z@y.z', g.user.address())
self.user.actor_as2 = {'type': 'Person'}
self.assertEqual('@y.z@y.z', self.user.address())
g.user.actor_as2 = {'type': 'Person'}
self.assertEqual('@y.z@y.z', g.user.address())
self.user.actor_as2 = {'url': 'http://foo'}
self.assertEqual('@y.z@y.z', self.user.address())
g.user.actor_as2 = {'url': 'http://foo'}
self.assertEqual('@y.z@y.z', g.user.address())
self.user.actor_as2 = {'url': ['http://foo', 'acct:bar@foo', 'acct:baz@y.z']}
self.assertEqual('@baz@y.z', self.user.address())
g.user.actor_as2 = {'url': ['http://foo', 'acct:bar@foo', 'acct:baz@y.z']}
self.assertEqual('@baz@y.z', g.user.address())
def test_actor_id(self):
with app.test_request_context('/'):
self.assertEqual('http://localhost/y.z', self.user.actor_id())
self.assertEqual('http://localhost/y.z', g.user.actor_id())
def _test_verify(self, redirects, hcard, actor, redirects_error=None):
with app.test_request_context('/'):
got = self.user.verify()
self.assertEqual(self.user.key, got.key)
got = g.user.verify()
self.assertEqual(g.user.key, got.key)
with self.subTest(redirects=redirects, hcard=hcard, actor=actor,
redirects_error=redirects_error):
self.assert_equals(redirects, bool(self.user.has_redirects))
self.assert_equals(hcard, bool(self.user.has_hcard))
self.assert_equals(redirects, bool(g.user.has_redirects))
self.assert_equals(hcard, bool(g.user.has_hcard))
if actor is None:
self.assertIsNone(self.user.actor_as2)
self.assertIsNone(g.user.actor_as2)
else:
got = {k: v for k, v in self.user.actor_as2.items()
got = {k: v for k, v in g.user.actor_as2.items()
if k in actor}
self.assert_equals(actor, got)
self.assert_equals(redirects_error, self.user.redirects_error)
self.assert_equals(redirects_error, g.user.redirects_error)
@mock.patch('requests.get')
def test_verify_neither(self, mock_get):
@ -176,9 +180,8 @@ http://this/404s
empty, empty,
]
with app.test_request_context('/'):
got = www_user.verify()
self.assertEqual('y.z', got.key.id())
got = www_user.verify()
self.assertEqual('y.z', got.key.id())
root_user = User.get_by_id('y.z')
self.assertEqual(root_user.key, www_user.key.get().use_instead)
@ -240,50 +243,57 @@ http://this/404s
})
def test_homepage(self):
self.assertEqual('https://y.z/', self.user.homepage)
self.assertEqual('https://y.z/', g.user.homepage)
def test_is_homepage(self):
for url in 'y.z', '//y.z', 'http://y.z', 'https://y.z':
self.assertTrue(self.user.is_homepage(url), url)
self.assertTrue(g.user.is_homepage(url), url)
for url in None, '', 'y', 'z', 'z.z', 'ftp://y.z', 'http://y', '://y.z':
self.assertFalse(self.user.is_homepage(url), url)
self.assertFalse(g.user.is_homepage(url), url)
class ObjectTest(testutil.TestCase):
def setUp(self):
super().setUp()
self.app_context = app.test_request_context('/')
self.app_context.push()
g.user = None
def tearDown(self):
self.app_context.pop()
super().tearDown()
def test_proxy_url(self):
with app.test_request_context('/'):
obj = Object(id='abc', as2={})
self.assertEqual('http://localhost/render?id=abc', obj.proxy_url())
obj = Object(id='abc', as2={})
self.assertEqual('http://localhost/render?id=abc', obj.proxy_url())
def test_actor_link(self):
with app.test_request_context('/'):
for expected, as2 in (
('href="">', {}),
('href="http://foo">foo', {'actor': 'http://foo'}),
('href="">Alice', {'actor': {'name': 'Alice'}}),
('href="http://foo">Alice', {'actor': {
'name': 'Alice',
'url': 'http://foo',
}}),
("""\
title="Alice">
<img class="profile" src="http://pic" />
Alice""", {'actor': {
'name': 'Alice',
'icon': {'type': 'Image', 'url': 'http://pic'},
}}),
):
obj = Object(id='x', as2=as2)
self.assert_multiline_in(expected, obj.actor_link())
for expected, as2 in (
('href="">', {}),
('href="http://foo">foo', {'actor': 'http://foo'}),
('href="">Alice', {'actor': {'name': 'Alice'}}),
('href="http://foo">Alice', {'actor': {
'name': 'Alice',
'url': 'http://foo',
}}),
("""\
title="Alice">
<img class="profile" src="http://pic" />
Alice""", {'actor': {
'name': 'Alice',
'icon': {'type': 'Image', 'url': 'http://pic'},
}}),
):
obj = Object(id='x', as2=as2)
self.assert_multiline_in(expected, obj.actor_link())
def test_actor_link_user(self):
user = User(id='user.com', actor_as2={"name": "Alice"})
g.user = User(id='user.com', actor_as2={"name": "Alice"})
obj = Object(id='x', source_protocol='ui', domains=['user.com'])
self.assertIn(
'href="/user/user.com"><img src="" class="profile"> Alice</a>',
obj.actor_link(user))
obj.actor_link())
def test_put_updates_get_object_cache(self):
obj = Object(id='x', as2={})

Wyświetl plik

@ -1,6 +1,7 @@
"""Unit tests for protocol.py."""
from unittest.mock import patch
from flask import g
from oauth_dropins.webutil.testutil import requests_response
import requests
@ -29,6 +30,7 @@ class ProtocolTest(testutil.TestCase):
self.user = self.make_user('foo.com', has_hcard=True)
self.app_context = app.test_request_context('/')
self.app_context.__enter__()
g.user = None
def tearDown(self):
self.app_context.__exit__(None, None, None)
@ -41,7 +43,7 @@ class ProtocolTest(testutil.TestCase):
# user.com webmention discovery
mock_get.return_value = requests_response('<html></html>')
Protocol.receive(REPLY['id'], user=self.user, as2=REPLY)
Protocol.receive(REPLY['id'], as2=REPLY)
self.assert_object(REPLY['id'],
as2=REPLY,

Wyświetl plik

@ -4,6 +4,7 @@ import datetime
import unittest
from unittest.mock import ANY, call
from flask import g
from granary import as2
from granary.tests.test_as1 import (
COMMENT,
@ -29,11 +30,11 @@ class FakeProtocol(protocol.Protocol):
LABEL = 'fake'
@classmethod
def send(cls, url, activity, *, user=None, log_data=True):
def send(cls, url, activity, log_data=True):
raise NotImplementedError()
@classmethod
def fetch(cls, id, obj, *, user=None):
def fetch(cls, id, obj):
raise NotImplementedError()

Wyświetl plik

@ -8,7 +8,7 @@ import logging
import re
import urllib.parse
from flask import render_template, request
from flask import g, render_template, request
from granary import as2
from oauth_dropins.webutil import flask_util, util
from oauth_dropins.webutil.flask_util import error
@ -38,22 +38,22 @@ class Actor(flask_util.XrdOrJrd):
if domain.split('.')[-1] in NON_TLDS:
error(f"{domain} doesn't look like a domain", status=404)
user = User.get_by_id(domain)
if not user:
g.user = User.get_by_id(domain)
if not g.user:
error(f'No user for {domain}', status=404)
logger.info(f'Generating WebFinger data for {domain}')
actor = user.to_as1() or {}
actor = g.user.to_as1() or {}
logger.info(f'AS1 actor: {actor}')
urls = util.dedupe_urls(util.get_list(actor, 'urls') +
util.get_list(actor, 'url') +
[user.homepage])
[g.user.homepage])
logger.info(f'URLs: {urls}')
canonical_url = urls[0]
# generate webfinger content
data = util.trim_nulls({
'subject': 'acct:' + user.address().lstrip('@'),
'subject': 'acct:' + g.user.address().lstrip('@'),
'aliases': urls,
'links':
[{
@ -80,7 +80,7 @@ class Actor(flask_util.XrdOrJrd):
# WARNING: in python 2 sometimes request.host_url lost port,
# http://localhost:8080 would become just http://localhost. no
# clue how or why. pay attention here if that happens again.
'href': user.actor_id(),
'href': g.user.actor_id(),
}, {
# AP reads this and sharedInbox from the AS2 actor, not
# webfinger, so strictly speaking, it's probably not needed here.

Wyświetl plik

@ -1,14 +1,10 @@
"""Handles inbound webmentions.
TODO tests:
* actor/attributedTo could be string URL
"""
"""Handles inbound webmentions."""
import logging
import urllib.parse
from urllib.parse import urlencode
import feedparser
from flask import redirect, request
from flask import g, redirect, request
from flask.views import View
from google.cloud.ndb import Key
from granary import as1, as2, microformats2
@ -39,7 +35,7 @@ class Webmention(View):
LABEL = 'webmention'
@classmethod
def send(cls, obj, url, *, user=None):
def send(cls, obj, url):
"""Sends a webmention to a given target URL."""
source_url = obj.proxy_url()
logger.info(f'Sending webmention from {source_url} to {url}')
@ -54,7 +50,7 @@ class Webmention(View):
return False
@classmethod
def fetch(cls, obj, id, *, user=None):
def fetch(cls, obj, id):
"""Fetches a URL over HTTP."""
parsed = util.fetch_mf2(id, gateway=True)
obj.mf2 = mf2util.find_first_entry(parsed, ['h-entry'])
@ -71,7 +67,6 @@ class WebmentionView(View):
source_mf2 = None # parsed mf2 dict
source_as1 = None # AS1 dict
source_as2 = None # AS2 dict
user = None # User
def dispatch_request(self):
logger.info(f'Params: {list(request.form.items())}')
@ -80,14 +75,14 @@ class WebmentionView(View):
domain = util.domain_from_link(source, minimize=False)
logger.info(f'webmention from {domain}')
self.user = User.get_by_id(domain)
if not self.user:
g.user = User.get_by_id(domain)
if not g.user:
error(f'No user found for domain {domain}')
# if source is home page, send an actor Update to followers' instances
if self.user.is_homepage(source):
if g.user.is_homepage(source):
self.source_url = source
self.source_mf2, actor_as1, actor_as2 = common.actor(self.user)
self.source_mf2, actor_as1, actor_as2 = common.actor(g.user)
id = common.host_url(f'{source}#update-{util.now().isoformat()}')
self.source_as1 = {
'objectType': 'activity',
@ -102,7 +97,7 @@ class WebmentionView(View):
'id': id,
'url': id,
'object': actor_as2,
}, user=self.user)
})
return self.try_activitypub() or 'No ActivityPub targets'
# fetch source page
@ -194,7 +189,7 @@ class WebmentionView(View):
status='in progress')
obj.populate(
domains=[self.user.key.id()],
domains=[g.user.key.id()],
source_protocol='webmention',
labels=['user'],
)
@ -221,12 +216,12 @@ class WebmentionView(View):
if not self.source_as2:
self.source_as2 = activitypub.postprocess_as2(
as2.from_as1(self.source_as1), target=target_as2, user=self.user)
as2.from_as1(self.source_as1), target=target_as2)
orig_actor = self.source_as2.get('actor')
if orig_actor:
logging.info(f'Overriding actor with {self.user.actor_id()}; was {orig_actor}')
self.source_as2['actor'] = self.user.actor_id()
logging.info(f'Overriding actor with {g.user.actor_id()}; was {orig_actor}')
self.source_as2['actor'] = g.user.actor_id()
if changed:
self.source_as2['type'] = 'Update'
@ -245,14 +240,14 @@ class WebmentionView(View):
# https://github.com/snarfed/bridgy-fed/issues/307
dest = ((target_as2.get('id') or util.get_first(target_as2, 'url'))
if target_as2 else util.get_url(self.source_as1, 'object'))
Follower.get_or_create(dest=dest, src=self.user.key.id(),
Follower.get_or_create(dest=dest, src=g.user.key.id(),
last_follow=self.source_as2)
try:
last = activitypub.ActivityPub.send(
# TODO: use obj
Object(as2=self.source_as2),
inbox, user=self.user, log_data=log_data)
inbox, log_data=log_data)
obj.delivered.append(target)
last_success = last
except BaseException as e:
@ -315,13 +310,13 @@ class WebmentionView(View):
)
# not actually an error
msg = ("Updating profile on followers' instances..."
if self.user.is_homepage(self.source_url)
if g.user.is_homepage(self.source_url)
else 'Delivering to followers...')
# TODO: switch this to return so that it doesn't log error
error(msg, status=202)
inboxes = set()
domain = self.user.key.id()
domain = g.user.key.id()
for follower in Follower.query().filter(
Follower.key > Key('Follower', domain + ' '),
Follower.key < Key('Follower', domain + chr(ord(' ') + 1))):
@ -343,7 +338,7 @@ class WebmentionView(View):
# fetch target page as AS2 object
try:
# TODO: make this generic across protocols
target_obj = activitypub.ActivityPub.get_object(target, user=self.user).as2
target_obj = activitypub.ActivityPub.get_object(target).as2
except (requests.HTTPError, BadGateway) as e:
resp = getattr(e, 'requests_response', None)
if resp and resp.ok:
@ -368,7 +363,7 @@ class WebmentionView(View):
if not inbox_url:
# fetch actor as AS object
# TODO: make this generic across protocols
actor = activitypub.ActivityPub.get_object(actor, user=self.user).as2
actor = activitypub.ActivityPub.get_object(actor).as2
inbox_url = actor.get('inbox')
if not inbox_url:
@ -399,7 +394,7 @@ class WebmentionInteractive(WebmentionView):
flash('OK')
except HTTPException as e:
flash(util.linkify(str(e.description), pretty=True))
path = f'/user/{self.user.key.id()}' if self.user else '/'
path = f'/user/{g.user.key.id()}' if g.user else '/'
return redirect(path, code=302)

Wyświetl plik

@ -3,6 +3,7 @@ import logging
import json
import re
from flask import g
from granary import microformats2, bluesky
import mf2util
from oauth_dropins.webutil import util
@ -23,13 +24,13 @@ def getProfile(input, actor=None):
if not actor or not re.match(util.DOMAIN_RE, actor):
raise ValueError(f'{actor} is not a domain')
user = User.get_by_id(actor)
if not user:
g.user = User.get_by_id(actor)
if not g.user:
raise ValueError(f'User {actor} not found')
elif not user.actor_as2:
elif not g.user.actor_as2:
return ValueError(f'User {actor} not fully set up')
actor_as1 = user.to_as1()
actor_as1 = g.user.to_as1()
logger.info(f'AS1 actor: {json.dumps(actor_as1, indent=2)}')
profile = {

Wyświetl plik

@ -3,6 +3,7 @@ import json
import logging
import re
from flask import g
from granary import bluesky, microformats2
import mf2util
from oauth_dropins.webutil import util
@ -21,10 +22,10 @@ def getAuthorFeed(input, author=None, limit=None, before=None):
if not author or not re.match(util.DOMAIN_RE, author):
raise ValueError(f'{author} is not a domain')
user = User.get_by_id(author)
if not user:
g.user = User.get_by_id(author)
if not g.user:
raise ValueError(f'User {author} not found')
elif not user.actor_as2:
elif not g.user.actor_as2:
return ValueError(f'User {author} not fully set up')
# TODO: unify with pages.feed?
@ -96,11 +97,11 @@ def getTimeline(input, algorithm=None, limit=50, before=None):
lexicons/app/bsky/feed/getTimeline.json
"""
# TODO: how to get authed user?
user = 'user.com'
domain = 'user.com'
# TODO: de-dupe with pages.feed()
logger.info(f'Fetching {limit} objects for {user}')
objects, _, _ = Object.query(Object.domains == user, Object.labels == 'feed') \
logger.info(f'Fetching {limit} objects for {domain}')
objects, _, _ = Object.query(Object.domains == domain, Object.labels == 'feed') \
.order(-Object.created) \
.fetch_page(limit)