forked from mirrors/bookwyrm
minor code cleanup and commenting
This commit is contained in:
parent
ef2209e77b
commit
3998c662cc
6 changed files with 76 additions and 49 deletions
|
@ -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
|
||||
user = models.User.objects.create_user(
|
||||
username, '', '',
|
||||
name=data.get('name'),
|
||||
summary=data.get('summary'),
|
||||
inbox=data['inbox'],
|
||||
outbox=data['outbox'],
|
||||
shared_inbox=shared_inbox,
|
||||
public_key=data.get('publicKey').get('publicKeyPem'),
|
||||
actor=actor,
|
||||
local=False
|
||||
)
|
||||
|
||||
try:
|
||||
user = models.User.objects.create_user(
|
||||
username,
|
||||
'', '', # email and passwords are left blank
|
||||
actor=actor,
|
||||
name=data.get('name'),
|
||||
summary=data.get('summary'),
|
||||
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'),
|
||||
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,
|
||||
|
|
|
@ -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())
|
||||
signer.verify(digest, signature)
|
||||
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):
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,13 +79,17 @@ 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:
|
||||
response = requests.get(OL_URL + olkey + '.json')
|
||||
data = response.json()
|
||||
author = Author(openlibrary_key=olkey, data=data)
|
||||
author.save()
|
||||
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()
|
||||
return author
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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),
|
||||
|
||||
|
|
Loading…
Reference in a new issue