minor code cleanup and commenting

This commit is contained in:
Mouse Reeve 2020-02-07 15:11:53 -08:00
parent ef2209e77b
commit 3998c662cc
6 changed files with 76 additions and 49 deletions

View file

@ -19,34 +19,46 @@ def get_or_create_remote_user(actor):
except models.User.DoesNotExist:
pass
# get the user's info
# load the user's info from the actor url
response = requests.get(
actor,
headers={'Accept': 'application/activity+json'}
)
data = response.json()
# the webfinger format for the username.
# TODO: get the user's domain in a better way
username = '%s@%s' % (actor.split('/')[-1], actor.split('/')[2])
shared_inbox = data.get('endpoints').get('sharedInbox') if \
data.get('endpoints') else None
try:
user = models.User.objects.create_user(
username, '', '',
username,
'', '', # email and passwords are left blank
actor=actor,
name=data.get('name'),
summary=data.get('summary'),
inbox=data['inbox'],
outbox=data['outbox'],
inbox=data['inbox'], #fail if there's no inbox
outbox=data['outbox'], # fail if there's no outbox
shared_inbox=shared_inbox,
# TODO: probably shouldn't bother to store this for remote users
public_key=data.get('publicKey').get('publicKeyPem'),
actor=actor,
local=False
)
except KeyError:
return False
return user
def get_recipients(user, post_privacy, direct_recipients=None):
''' deduplicated list of recipients '''
''' deduplicated list of recipient inboxes '''
recipients = direct_recipients or []
if post_privacy == 'direct':
# all we care about is direct_recipients, not followers
return recipients
# load all the followers of the user who is sending the message
followers = user.followers.all()
if post_privacy == 'public':
# post to public shared inboxes
@ -58,28 +70,28 @@ def get_recipients(user, post_privacy, direct_recipients=None):
# don't send it to the shared inboxes
inboxes = set(u.inbox for u in followers)
recipients += list(inboxes)
# if post privacy is direct, we just have direct recipients,
# which is already set. hurray
return recipients
def broadcast(sender, action, recipients):
def broadcast(sender, activity, recipients):
''' send out an event '''
errors = []
for recipient in recipients:
try:
sign_and_send(sender, action, recipient)
sign_and_send(sender, activity, recipient)
except requests.exceptions.HTTPError as e:
# TODO: maybe keep track of users who cause errors
errors.append({
'error': e,
'recipient': recipient,
'activity': action,
'activity': activity,
})
return errors
def sign_and_send(sender, action, destination):
def sign_and_send(sender, activity, destination):
''' crpyto whatever and http junk '''
# TODO: handle http[s] with regex
inbox_fragment = sender.inbox.replace('https://%s' % DOMAIN, '')
now = datetime.utcnow().isoformat()
signature_headers = [
@ -88,6 +100,8 @@ def sign_and_send(sender, action, destination):
'date: %s' % now
]
message_to_sign = '\n'.join(signature_headers)
# TODO: raise an error if the user doesn't have a private key
signer = pkcs1_15.new(RSA.import_key(sender.private_key))
signed_message = signer.sign(SHA256.new(message_to_sign.encode('utf8')))
@ -101,7 +115,7 @@ def sign_and_send(sender, action, destination):
response = requests.post(
destination,
data=json.dumps(action),
data=json.dumps(activity),
headers={
'Date': now,
'Signature': signature,

View file

@ -16,6 +16,11 @@ from fedireads.openlibrary import get_or_create_book
from fedireads.settings import DOMAIN
class HttpResponseUnauthorized(HttpResponse):
''' http response for authentication failure '''
status_code = 401
def webfinger(request):
''' allow other servers to ask about a user '''
resource = request.GET.get('resource')
@ -66,7 +71,7 @@ def shared_inbox(request):
headers={'Accept': 'application/activity+json'}
)
if not response.ok:
response.raise_for_status()
return HttpResponseUnauthorized()
actor = response.json()
key = RSA.import_key(actor['publicKey']['publicKeyPem'])
@ -85,7 +90,11 @@ def shared_inbox(request):
signer = pkcs1_15.new(key)
digest = SHA256.new()
digest.update(comparison_string.encode())
try:
signer.verify(digest, signature)
except:
# TODO: what kind of error does this throw?
return HttpResponseUnauthorized()
if activity['type'] == 'Add':
return handle_incoming_shelve(activity)
@ -137,7 +146,7 @@ def get_actor(request, username):
'publicKeyPem': user.public_key,
},
'endpoints': {
'sharedInbox': 'https://%s/inbox' % DOMAIN,
'sharedInbox': user.shared_inbox,
}
})
@ -150,43 +159,34 @@ def get_followers(request, username):
user = models.User.objects.get(localname=username)
followers = user.followers
id_slug = '%s/followers' % user.actor
if request.GET.get('page'):
page = request.GET.get('page')
return JsonResponse(get_follow_page(followers, id_slug, page))
follower_count = followers.count()
return JsonResponse({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': id_slug,
'type': 'OrderedCollection',
'totalItems': follower_count,
'first': '%s?page=1' % id_slug,
})
return format_follow_info(user, request.GET.get('page'), followers)
@csrf_exempt
def get_following(request, username):
''' return a list of following for an actor '''
# TODO: this is total deplication of get_followers, should be streamlined
if request.method != 'GET':
return HttpResponseBadRequest()
user = models.User.objects.get(localname=username)
following = models.User.objects.filter(followers=user)
return format_follow_info(user, request.GET.get('page'), following)
def format_follow_info(user, page, follow_queryset):
''' create the activitypub json for followers/following '''
id_slug = '%s/following' % user.actor
if request.GET.get('page'):
page = request.GET.get('page')
return JsonResponse(get_follow_page(following, id_slug, page))
following_count = following.count()
if page:
return JsonResponse(get_follow_page(follow_queryset, id_slug, page))
count = follow_queryset.count()
return JsonResponse({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': id_slug,
'type': 'OrderedCollection',
'totalItems': following_count,
'totalItems': count,
'first': '%s?page=1' % id_slug,
})
def get_follow_page(user_list, id_slug, page):
''' format a list of followers/following '''
page = int(page)
@ -259,6 +259,7 @@ def handle_incoming_follow(activity):
activity_type='Follow',
)
uuid = uuid4()
# TODO does this need to be signed?
return JsonResponse({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://%s/%s' % (DOMAIN, uuid),
@ -277,6 +278,7 @@ def handle_incoming_create(activity):
'inReplyTo' in activity['object']:
possible_book = activity['object']['inReplyTo']
try:
# TODO idk about this error handling, should probs be more granular
book = get_or_create_book(possible_book)
models.Review(
uuid=uuid,
@ -318,6 +320,7 @@ def handle_incoming_accept(activity):
followed=followed,
content=activity,
).save()
return HttpResponse()
def handle_response(response):

View file

@ -211,6 +211,7 @@ class Book(models.Model):
openlibrary_key = models.CharField(max_length=255, unique=True)
data = JSONField()
authors = models.ManyToManyField('Author')
# TODO: also store cover thumbnail
cover = models.ImageField(upload_to='covers/', blank=True, null=True)
shelves = models.ManyToManyField(
'Shelf',

View file

@ -23,9 +23,10 @@ def book_search(query):
})
return results
def get_or_create_book(olkey, user=None, update=False):
''' add a book '''
# TODO: check if this is a valid open library key, and a book
# TODO: check if this is a valid open library key, and a work
olkey = olkey
# get the existing entry from our db, if it exists
@ -65,6 +66,7 @@ def get_or_create_book(olkey, user=None, update=False):
def get_cover(cover_id):
''' ask openlibrary for the cover '''
# TODO: get medium and small versions
image_name = '%s-M.jpg' % cover_id
url = 'https://covers.openlibrary.org/b/id/%s' % image_name
response = requests.get(url)
@ -77,11 +79,15 @@ def get_cover(cover_id):
def get_or_create_author(olkey):
''' load that author '''
# TODO: validate that this is an author key
# TODO: error handling
try:
author = Author.objects.get(openlibrary_key=olkey)
except ObjectDoesNotExist:
pass
response = requests.get(OL_URL + olkey + '.json')
if not response.ok:
response.raise_for_status()
data = response.json()
author = Author(openlibrary_key=olkey, data=data)
author.save()

View file

@ -65,6 +65,7 @@ def handle_outgoing_follow(user, to_follow):
errors = broadcast(user, activity, [to_follow.inbox])
for error in errors:
# TODO: following masto users is returning 400
raise(error['error'])
@ -166,10 +167,11 @@ def handle_review(user, book, name, content, rating):
'published': datetime.utcnow().isoformat(),
'attributedTo': user.actor,
'content': content,
'inReplyTo': book.openlibrary_key,
'inReplyTo': book.openlibrary_key, # TODO is this the right identifier?
'rating': rating, # fedireads-only custom field
'to': 'https://www.w3.org/ns/activitystreams#Public'
}
# TODO: create alt version for mastodon
recipients = get_recipients(user, 'public')
create_uuid = uuid4()
activity = {

View file

@ -16,6 +16,7 @@ urlpatterns = [
re_path(r'^user/(?P<username>\w+)/outbox/?$', outgoing.outbox),
re_path(r'^user/(?P<username>\w+)/followers/?$', incoming.get_followers),
re_path(r'^user/(?P<username>\w+)/following/?$', incoming.get_following),
# TODO: shelves need pages in the UI and for their activitypub Collection
re_path(r'^.well-known/webfinger/?$', incoming.webfinger),
# TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta),