Skip to content

Commit

Permalink
AP users: finish migrating Follower from string domains/ids to User keys
Browse files Browse the repository at this point in the history
for #512
  • Loading branch information
snarfed committed Jun 8, 2023
1 parent 9cb8c1f commit 7c82bf7
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 117 deletions.
16 changes: 6 additions & 10 deletions activitypub.py
Expand Up @@ -645,20 +645,16 @@ def follower_collection(protocol, domain, collection):
https://www.w3.org/TR/activitypub/#collections
https://www.w3.org/TR/activitystreams-core/#paging
"""
if not PROTOCOLS[protocol].get_by_id(domain):
g.user = PROTOCOLS[protocol].get_by_id(domain)
if not g.user:
return f'{protocol} user {domain} not found', 404

# page
followers, new_before, new_after = Follower.fetch_page(domain, collection)
items = []
for f in followers:
if f.obj.as1:
items.append(as2.from_as1(f.obj.as1))

followers, new_before, new_after = Follower.fetch_page(collection)
page = {
'type': 'CollectionPage',
'partOf': request.base_url,
'items': items,
'items': [f.user.actor_as2 for f in followers if f.user.actor_as2],
}
if new_before:
page['next'] = f'{request.base_url}?before={new_before}'
Expand All @@ -674,10 +670,10 @@ def follower_collection(protocol, domain, collection):
return page, {'Content-Type': as2.CONTENT_TYPE}

# collection
domain_prop = Follower.dest if collection == 'followers' else Follower.src
prop = Follower.to if collection == 'followers' else Follower.from_
count = Follower.query(
Follower.status == 'active',
domain_prop == domain,
prop == g.user.key,
).count()

collection = {
Expand Down
20 changes: 14 additions & 6 deletions models.py
Expand Up @@ -533,7 +533,7 @@ def get_or_create(cls, *, from_, to, **kwargs):

@staticmethod
def fetch_page(collection):
"""Fetches a page of Follower entities for the current user.
"""Fetches a page of Followers for the current user.
Wraps :func:`fetch_page`. Paging uses the `before` and `after` query
parameters, if available in the request.
Expand All @@ -542,19 +542,27 @@ def fetch_page(collection):
collection, str, 'followers' or 'following'
Returns:
(results, new_before, new_after) tuple with:
results: list of :class:`Follower` entities
(followers, new_before, new_after) tuple with:
followers: list of :class:`Follower` entities, annotated with an extra
`user` attribute that holds the follower or following :class:`User`
new_before, new_after: str query param values for `before` and `after`
to fetch the previous and next pages, respectively
"""
assert collection in ('followers', 'following'), collection

prop = Follower.to if collection == 'followers' else Follower.from_
filter_prop = Follower.to if collection == 'followers' else Follower.from_
query = Follower.query(
Follower.status == 'active',
prop == g.user.key,
filter_prop == g.user.key,
).order(-Follower.updated)
return fetch_page(query, Follower)

followers, before, after = fetch_page(query, Follower)
users = ndb.get_multi(f.from_ if collection == 'followers' else f.to
for f in followers)
for f, u in zip(followers, users):
f.user = u

return followers, before, after


def fetch_page(query, model_class):
Expand Down
21 changes: 7 additions & 14 deletions pages.py
Expand Up @@ -59,7 +59,11 @@ def load_user(protocol, id):
error('', status=302, location=g.user.user_page_path())

if not g.user or not g.user.direct:
error(USER_NOT_FOUND_HTML, status=404)
# 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

Expand Down Expand Up @@ -120,23 +124,12 @@ def followers_or_following(protocol, id, collection):
load_user(protocol, id)

followers, before, after = Follower.fetch_page(collection)
users = {
u.key: u for u in get_multi(f.from_ if collection == 'followers' else f.to
for f in followers)
}

for f in followers:
user = users[f.from_ if collection == 'followers' else f.to]
f.url = user.web_url()
f.as1 = as2.to_as1(user.actor_as2)
f.handle = as2.address(user.actor_as2 or f.url) or f.url
f.picture = util.get_url(f.as1, 'icon') or util.get_url(f.as1, 'image')

return render_template(
f'{collection}.html',
util=util,
address=request.args.get('address'),
as2=as2,
g=g,
util=util,
**locals()
)

Expand Down
35 changes: 22 additions & 13 deletions protocol.py
Expand Up @@ -173,9 +173,14 @@ def receive(cls, id, **props):
error(f'Undo of Follow requires actor id and object id. Got: {actor_id} {inner_obj_id} {obj.as1}')

# deactivate Follower
# TODO(#512): generalize across protocols
# TODO(#512): merge Protocol and User
followee_domain = util.domain_from_link(inner_obj_id, minimize=False)
follower = Follower.get_by_id(
Follower._id(dest=followee_domain, src=actor_id))
from web import Web
follower = Follower.query(
Follower.to == Web(id=followee_domain).key,
Follower.from_ == cls(id=actor_id).key,
Follower.status == 'active').get()
if follower:
logger.info(f'Marking {follower} inactive')
follower.status = 'inactive'
Expand Down Expand Up @@ -209,9 +214,12 @@ def receive(cls, id, **props):

# assume this is an actor
# https://github.com/snarfed/bridgy-fed/issues/63
# TODO(#512): generalize across protocols
logger.info(f'Deactivating Followers with src or dest = {inner_obj_id}')
followers = Follower.query(OR(Follower.src == inner_obj_id,
Follower.dest == inner_obj_id)
from activitypub import ActivityPub
deleted_user = ActivityPub(id=inner_obj_id).key
followers = Follower.query(OR(Follower.to == deleted_user,
Follower.from_ == deleted_user)
).fetch()
for f in followers:
f.status = 'inactive'
Expand Down Expand Up @@ -240,11 +248,11 @@ def receive(cls, id, **props):
if (actor and actor_id and
(obj.type == 'share' or obj.type in ('create', 'post') and not is_reply)):
logger.info(f'Delivering to followers of {actor_id}')
for f in Follower.query(Follower.dest == actor_id,
Follower.status == 'active',
projection=[Follower.src]):
if f.src not in obj.domains:
obj.domains.append(f.src)
from activitypub import ActivityPub
for f in Follower.query(Follower.to == ActivityPub(id=actor_id).key,
Follower.status == 'active'):
if f.from_.id() not in obj.domains:
obj.domains.append(f.from_.id())
if obj.domains and 'feed' not in obj.labels:
obj.labels.append('feed')

Expand Down Expand Up @@ -272,10 +280,11 @@ def accept_follow(cls, obj):
error(f'Follow actor requires id and inbox. Got: {follower}')

# store Follower
follower_obj = Follower.get_or_create(
dest=g.user.key.id(), src=follower_id, last_follow=obj.as2)
follower_obj.status = 'active'
follower_obj.put()
# TODO(#512): generalize across protocols
from activitypub import ActivityPub
from_ = ActivityPub.get_or_create(id=follower_id, actor_as2=obj.as2)
follower_obj = Follower.get_or_create(to=g.user, from_=from_, follow=obj.key,
status='active')

# send Accept
followee_actor_url = g.user.ap_actor()
Expand Down
19 changes: 12 additions & 7 deletions templates/_followers.html
Expand Up @@ -3,13 +3,18 @@

{% for f in followers %}
<li class="row">
<a class="follower col-xs-10 col-sm-6 col-lg-6" href="{{ f.url }}">
{% if f.picture %}
<img class="profile u-photo" src="{{ f.picture }}" width="48px">
{% endif %}
{{ f.as1.get('displayName') or '' }}
{{ f.handle }}
</a>
{% with url=f.user.web_url(), user_as1=as2.to_as1(f.user.actor_as2) %}
<a class="follower col-xs-10 col-sm-6 col-lg-6" href="{{ url }}">
{% with picture=util.get_url(user_as1, 'icon') or util.get_url(user_as1, 'image') %}
{% if picture %}
<img class="profile u-photo" src="{{ picture }}" width="48px">
{% endif %}
{% endwith %}
{{ user_as1.get('displayName') or '' }}
{{ as2.address(f.user.actor_as2 or url) or url }}
</a>
{% endwith %}

{% if page_name == 'following' %}
<form method="post" action="/unfollow/start" class="col-xs-2 col-sm-1 col-lg-1">
<input type="hidden" name="me" value="https://{{ domain }}/" />
Expand Down

0 comments on commit 7c82bf7

Please sign in to comment.