code style cleanup

This commit is contained in:
Mouse Reeve 2020-03-29 00:05:09 -07:00
parent 3ead02e05f
commit 92790d520f
13 changed files with 113 additions and 77 deletions

View file

@ -1,3 +1,4 @@
''' we need this file to initialize celery '''
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
# This will make sure the app is always imported when # This will make sure the app is always imported when

View file

@ -13,7 +13,12 @@ from fedireads import models
def get_recipients(user, post_privacy, direct_recipients=None, limit=False): def get_recipients(user, post_privacy, direct_recipients=None, limit=False):
''' deduplicated list of recipient inboxes ''' ''' deduplicated list of recipient inboxes '''
recipients = direct_recipients or [] # we're always going to broadcast to any direct recipients
direct_recipients = direct_recipients or []
recipients = [u.inbox for u in direct_recipients]
# if we're federating a book, it isn't related to any user's followers, we
# just want to send it out. To whom? I'm not sure, but for now, everyone.
if not user: if not user:
users = models.User.objects.filter(local=False).all() users = models.User.objects.filter(local=False).all()
recipients += list(set( recipients += list(set(
@ -22,16 +27,19 @@ def get_recipients(user, post_privacy, direct_recipients=None, limit=False):
return recipients return recipients
if post_privacy == 'direct': if post_privacy == 'direct':
# all we care about is direct_recipients, not followers # all we care about is direct_recipients, not followers, so we're done
return [u.inbox for u in recipients] return recipients
# load all the followers of the user who is sending the message # load all the followers of the user who is sending the message
# "limit" refers to whether we want to send to other fedireads instances,
# or to only non-fedireads instances. this is confusing (TODO)
if not limit: if not limit:
followers = user.followers.all() followers = user.followers.all()
else: else:
fedireads_user = limit == 'fedireads' fedireads_user = limit == 'fedireads'
followers = user.followers.filter(fedireads_user=fedireads_user).all() followers = user.followers.filter(fedireads_user=fedireads_user).all()
# TODO I don't think this is actually accomplishing pubic/followers only?
if post_privacy == 'public': if post_privacy == 'public':
# post to public shared inboxes # post to public shared inboxes
shared_inboxes = set( shared_inboxes = set(
@ -39,11 +47,12 @@ def get_recipients(user, post_privacy, direct_recipients=None, limit=False):
) )
recipients += list(shared_inboxes) recipients += list(shared_inboxes)
recipients += [u.inbox for u in followers if not u.shared_inbox] recipients += [u.inbox for u in followers if not u.shared_inbox]
# TODO: direct to anyone who's mentioned
if post_privacy == 'followers': if post_privacy == 'followers':
# don't send it to the shared inboxes # don't send it to the shared inboxes
inboxes = set(u.inbox for u in followers) inboxes = set(u.inbox for u in followers)
recipients += list(inboxes) recipients += list(inboxes)
return recipients return recipients

View file

@ -1,3 +1,4 @@
''' handle reading a csv from goodreads '''
import re import re
import csv import csv
import itertools import itertools
@ -5,22 +6,28 @@ from requests import HTTPError
from fedireads import books_manager from fedireads import books_manager
# Mapping goodreads -> fedireads shelf titles. # Mapping goodreads -> fedireads shelf titles.
GOODREADS_SHELVES = { GOODREADS_SHELVES = {
'read': 'read', 'read': 'read',
'currently-reading': 'reading', 'currently-reading': 'reading',
'to-read': 'to-read', 'to-read': 'to-read',
} }
# TODO: remove or notify about this in the UI
MAX_ENTRIES = 20 MAX_ENTRIES = 20
def unquote_string(text): def unquote_string(text):
''' resolve csv quote weirdness '''
match = re.match(r'="([^"]*)"', text) match = re.match(r'="([^"]*)"', text)
if match: if match:
return match.group(1) return match.group(1)
else: else:
return text return text
def construct_search_term(title, author): def construct_search_term(title, author):
''' formulate a query for the data connector '''
# Strip brackets (usually series title from search term) # Strip brackets (usually series title from search term)
title = re.sub(r'\s*\([^)]*\)\s*', '', title) title = re.sub(r'\s*\([^)]*\)\s*', '', title)
# Open library doesn't like including author initials in search term. # Open library doesn't like including author initials in search term.
@ -28,7 +35,9 @@ def construct_search_term(title, author):
return ' '.join([title, author]) return ' '.join([title, author])
class GoodreadsCsv(object): class GoodreadsCsv(object):
''' define a goodreads csv '''
def __init__(self, csv_file): def __init__(self, csv_file):
self.reader = csv.DictReader(csv_file) self.reader = csv.DictReader(csv_file)
@ -41,30 +50,42 @@ class GoodreadsCsv(object):
pass pass
yield entry yield entry
class GoodreadsItem(object): class GoodreadsItem(object):
''' a processed line in a goodreads csv '''
def __init__(self, line): def __init__(self, line):
self.line = line self.line = line
self.book = None self.book = None
def resolve(self): def resolve(self):
''' try various ways to lookup a book '''
self.book = self.get_book_from_isbn() self.book = self.get_book_from_isbn()
if not self.book: if not self.book:
self.book = self.get_book_from_title_author() self.book = self.get_book_from_title_author()
def get_book_from_isbn(self): def get_book_from_isbn(self):
''' search by isbn '''
isbn = unquote_string(self.line['ISBN13']) isbn = unquote_string(self.line['ISBN13'])
search_results = books_manager.search(isbn) search_results = books_manager.search(isbn)
if search_results: if search_results:
return books_manager.get_or_create_book(search_results[0].key) return books_manager.get_or_create_book(search_results[0].key)
def get_book_from_title_author(self): def get_book_from_title_author(self):
search_term = construct_search_term(self.line['Title'], self.line['Author']) ''' search by title and author '''
search_term = construct_search_term(
self.line['Title'],
self.line['Author']
)
search_results = books_manager.search(search_term) search_results = books_manager.search(search_term)
if search_results: if search_results:
return books_manager.get_or_create_book(search_results[0].key) return books_manager.get_or_create_book(search_results[0].key)
@property @property
def shelf(self): def shelf(self):
''' the goodreads shelf field '''
if self.line['Exclusive Shelf']: if self.line['Exclusive Shelf']:
return GOODREADS_SHELVES[self.line['Exclusive Shelf']] return GOODREADS_SHELVES[self.line['Exclusive Shelf']]
@ -73,3 +94,4 @@ class GoodreadsItem(object):
def __str__(self): def __str__(self):
return "{} by {}".format(self.line['Title'], self.line['Author']) return "{} by {}".format(self.line['Title'], self.line['Author'])

View file

@ -1,17 +1,16 @@
''' handles all of the activity coming in to the server ''' ''' handles all of the activity coming in to the server '''
from base64 import b64decode from base64 import b64decode
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15 from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from django.http import HttpResponse, HttpResponseBadRequest, \
HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt
import django.db.utils import django.db.utils
from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt
import json import json
import requests import requests
from fedireads import models from fedireads import models, outgoing
from fedireads import outgoing
from fedireads import status as status_builder from fedireads import status as status_builder
from fedireads.remote_user import get_or_create_remote_user from fedireads.remote_user import get_or_create_remote_user
@ -35,23 +34,30 @@ def shared_inbox(request):
handlers = { handlers = {
'Follow': handle_follow, 'Follow': handle_follow,
'Create': handle_create,
'Accept': handle_follow_accept, 'Accept': handle_follow_accept,
'Reject': handle_follow_reject, 'Reject': handle_follow_reject,
'Create': handle_create,
'Like': handle_favorite, 'Like': handle_favorite,
'Add': handle_add, 'Add': handle_add,
} }
activity_type = activity['type'] activity_type = activity['type']
handler = None
if activity_type in handlers: if activity_type in handlers:
handler = handlers[activity_type] handler = handlers[activity_type]
elif activity_type == 'Undo' and 'object' in activity: elif activity_type == 'Undo' and 'object' in activity:
if activity['object']['type'] == 'Follow': if activity['object']['type'] == 'Follow':
handler = handle_undo handler = handle_unfollow
elif activity['object']['type'] == 'Like': elif activity['object']['type'] == 'Like':
handler = handle_unfavorite handler = handle_unfavorite
elif activity_type == 'Update' and 'object' in activity:
if activity['object']['type'] == 'Person':
handler = None# TODO: handle_update_user
elif activity_type['object']['type'] == 'Book':
handler = None# TODO: handle_update_book
if handler: if handler:
return handlers[activity_type](activity) return handler(activity)
return HttpResponseNotFound() return HttpResponseNotFound()
@ -152,7 +158,7 @@ def handle_follow(activity):
return HttpResponse() return HttpResponse()
def handle_undo(activity): def handle_unfollow(activity):
''' unfollow a local user ''' ''' unfollow a local user '''
obj = activity['object'] obj = activity['object']
if not obj['type'] == 'Follow': if not obj['type'] == 'Follow':
@ -210,36 +216,25 @@ def handle_create(activity):
if not 'object' in activity: if not 'object' in activity:
return HttpResponseBadRequest() return HttpResponseBadRequest()
if user.local:
# we really oughtn't even be sending in this case
return HttpResponse()
if activity['object'].get('fedireadsType') in ['Review', 'Comment'] and \
'inReplyToBook' in activity['object']:
try:
if activity['object']['fedireadsType'] == 'Review':
builder = status_builder.create_review_from_activity
else:
builder = status_builder.create_comment_from_activity
# create the status, it'll throw a valueerror if anything is missing
builder(user, activity['object'])
except ValueError:
return HttpResponseBadRequest()
else:
# TODO: should only create notes if they are relevent to a book, # TODO: should only create notes if they are relevent to a book,
# so, not every single thing someone posts on mastodon # so, not every single thing someone posts on mastodon
response = HttpResponse()
if activity['object'].get('fedireadsType') == 'Review' and \
'inReplyToBook' in activity['object']:
if user.local:
review_id = activity['object']['id'].split('/')[-1]
models.Review.objects.get(id=review_id)
else:
try:
status_builder.create_review_from_activity(
user,
activity['object']
)
except ValueError:
return HttpResponseBadRequest()
elif activity['object'].get('fedireadsType') == 'Comment' and \
'inReplyToBook' in activity['object']:
if user.local:
comment_id = activity['object']['id'].split('/')[-1]
models.Comment.objects.get(id=comment_id)
else:
try:
status_builder.create_comment_from_activity(
user,
activity['object']
)
except ValueError:
return HttpResponseBadRequest()
elif not user.local:
try: try:
status = status_builder.create_status_from_activity( status = status_builder.create_status_from_activity(
user, user,
@ -255,7 +250,7 @@ def handle_create(activity):
except ValueError: except ValueError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
return response return HttpResponse()
def handle_favorite(activity): def handle_favorite(activity):

View file

@ -2,5 +2,5 @@
from .book import Connector, Book, Work, Edition, Author from .book import Connector, Book, Work, Edition, Author
from .shelf import Shelf, ShelfBook from .shelf import Shelf, ShelfBook
from .status import Status, Review, Comment, Favorite, Tag, Notification from .status import Status, Review, Comment, Favorite, Tag, Notification
from .user import User, FederatedServer, UserFollows, UserFollowRequest, \ from .user import User, UserFollows, UserFollowRequest, UserBlocks
UserBlocks from .user import FederatedServer

View file

@ -7,10 +7,10 @@ from urllib.parse import urlencode
from fedireads import activitypub from fedireads import activitypub
from fedireads import models from fedireads import models
from fedireads.status import create_review, create_status, create_tag, \
create_notification, create_comment
from fedireads.remote_user import get_or_create_remote_user
from fedireads.broadcast import get_recipients, broadcast from fedireads.broadcast import get_recipients, broadcast
from fedireads.status import create_review, create_status, create_comment
from fedireads.status import create_tag, create_notification
from fedireads.remote_user import get_or_create_remote_user
@csrf_exempt @csrf_exempt
@ -110,6 +110,7 @@ def handle_accept(user, to_follow, follow_request):
recipient = get_recipients(to_follow, 'direct', direct_recipients=[user]) recipient = get_recipients(to_follow, 'direct', direct_recipients=[user])
broadcast(to_follow, activity, recipient) broadcast(to_follow, activity, recipient)
def handle_reject(user, to_follow, relationship): def handle_reject(user, to_follow, relationship):
''' a local user who managed follows rejects a follow request ''' ''' a local user who managed follows rejects a follow request '''
relationship.delete() relationship.delete()

View file

@ -13,7 +13,7 @@ def get_or_create_remote_user(actor):
except models.User.DoesNotExist: except models.User.DoesNotExist:
pass pass
# TODO: also bring in the user's prevous reviews and books # TODO: handle remote server and connector
# load the user's info from the actor url # load the user's info from the actor url
response = requests.get( response = requests.get(
@ -54,6 +54,7 @@ def get_or_create_remote_user(actor):
def get_remote_reviews(user): def get_remote_reviews(user):
''' ingest reviews by a new remote fedireads user ''' ''' ingest reviews by a new remote fedireads user '''
# TODO: use the server as the data source instead of OL
outbox_page = user.outbox + '?page=true' outbox_page = user.outbox + '?page=true'
response = requests.get( response = requests.get(
outbox_page, outbox_page,

View file

@ -1,8 +1,9 @@
''' Handle user activity ''' ''' Handle user activity '''
from django.db import IntegrityError
from fedireads import models from fedireads import models
from fedireads.books_manager import get_or_create_book from fedireads.books_manager import get_or_create_book
from fedireads.sanitize_html import InputHtmlParser from fedireads.sanitize_html import InputHtmlParser
from django.db import IntegrityError
def create_review_from_activity(author, activity): def create_review_from_activity(author, activity):
@ -113,9 +114,10 @@ def get_favorite(absolute_id):
def get_by_absolute_id(absolute_id, model): def get_by_absolute_id(absolute_id, model):
''' generalized function to get from a model with a remote_id field ''' ''' generalized function to get from a model with a remote_id field '''
# check if it's a remote status
if not absolute_id: if not absolute_id:
return None return None
# check if it's a remote status
try: try:
return model.objects.get(remote_id=absolute_id) return model.objects.get(remote_id=absolute_id)
except model.DoesNotExist: except model.DoesNotExist:

View file

@ -11,6 +11,7 @@ localname_regex = r'(?P<username>[\w\-_]+)'
user_path = r'^user/%s' % username_regex user_path = r'^user/%s' % username_regex
local_user_path = r'^user/%s' % localname_regex local_user_path = r'^user/%s' % localname_regex
status_path = r'%s/(status|review|comment)/(?P<status_id>\d+)' % local_user_path status_path = r'%s/(status|review|comment)/(?P<status_id>\d+)' % local_user_path
book_path = r'^book/(?P<book_identifier>[\w\-]+)'
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@ -25,7 +26,9 @@ urlpatterns = [
re_path(r'^.well-known/nodeinfo/?$', wellknown.nodeinfo_pointer), re_path(r'^.well-known/nodeinfo/?$', wellknown.nodeinfo_pointer),
re_path(r'^nodeinfo/2\.0/?$', wellknown.nodeinfo), re_path(r'^nodeinfo/2\.0/?$', wellknown.nodeinfo),
re_path(r'^api/v1/instance/?$', wellknown.instance_info), re_path(r'^api/v1/instance/?$', wellknown.instance_info),
re_path(r'^api/v1/instance/peers/?$', wellknown.peers),
# TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta), # TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta),
# TODO: robots.txt
# ui views # ui views
re_path(r'^login/?$', views.login_page), re_path(r'^login/?$', views.login_page),
@ -52,9 +55,9 @@ urlpatterns = [
re_path(r'%s/replies(.json)?/?$' % status_path, views.replies_page), re_path(r'%s/replies(.json)?/?$' % status_path, views.replies_page),
# books # books
re_path(r'^book/(?P<book_identifier>[\w\-]+)(.json)?/?$', views.book_page), re_path(r'%s(.json)?/?$' % book_path, views.book_page),
re_path(r'^book/(?P<book_identifier>[\w\-]+)/(?P<tab>friends|local|federated)?$', views.book_page), re_path(r'%s/(?P<tab>friends|local|federated)?$' % book_path, views.book_page),
re_path(r'^book/(?P<book_identifier>[\w\-]+)/edit/?$', views.edit_book_page), re_path(r'%s/edit/?$' % book_path, views.edit_book_page),
re_path(r'^author/(?P<author_identifier>[\w\-]+)/?$', views.author_page), re_path(r'^author/(?P<author_identifier>[\w\-]+)/?$', views.author_page),
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page), re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),

View file

@ -354,12 +354,10 @@ def import_data(request):
failures.append(item) failures.append(item)
outgoing.handle_import_books(request.user, results) outgoing.handle_import_books(request.user, results)
if failures:
return TemplateResponse(request, 'import_results.html', { return TemplateResponse(request, 'import_results.html', {
'success_count': len(results), 'success_count': len(results),
'failures': failures, 'failures': failures,
}) })
else:
return redirect('/')
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()

View file

@ -21,7 +21,6 @@ def get_user_from_username(username):
def is_api_request(request): def is_api_request(request):
''' check whether a request is asking for html or data ''' ''' check whether a request is asking for html or data '''
# TODO: this should probably be the full content type? maybe?
return 'json' in request.headers.get('Accept') or \ return 'json' in request.headers.get('Accept') or \
request.path[-5:] == '.json' request.path[-5:] == '.json'
@ -369,8 +368,6 @@ def book_page(request, book_identifier, tab='friends'):
'book', 'name', 'identifier' 'book', 'name', 'identifier'
).distinct().all() ).distinct().all()
review_form = forms.ReviewForm()
tag_form = forms.TagForm()
data = { data = {
'book': book, 'book': book,
'shelf': shelf, 'shelf': shelf,
@ -381,8 +378,8 @@ def book_page(request, book_identifier, tab='friends'):
'tags': tags, 'tags': tags,
'user_tags': user_tags, 'user_tags': user_tags,
'user_tag_names': user_tag_names, 'user_tag_names': user_tag_names,
'review_form': review_form, 'review_form': forms.ReviewForm(),
'tag_form': tag_form, 'tag_form': forms.TagForm(),
'feed_tabs': [ 'feed_tabs': [
{'id': 'friends', 'display': 'Friends'}, {'id': 'friends', 'display': 'Friends'},
{'id': 'local', 'display': 'Local'}, {'id': 'local', 'display': 'Local'},
@ -434,7 +431,6 @@ def tag_page(request, tag_id):
def shelf_page(request, username, shelf_identifier): def shelf_page(request, username, shelf_identifier):
''' display a shelf ''' ''' display a shelf '''
# TODO: json view
try: try:
user = get_user_from_username(username) user = get_user_from_username(username)
except models.User.DoesNotExist: except models.User.DoesNotExist:

View file

@ -44,12 +44,13 @@ def nodeinfo_pointer(request):
] ]
}) })
def nodeinfo(request): def nodeinfo(request):
''' basic info about the server ''' ''' basic info about the server '''
if request.method != 'GET': if request.method != 'GET':
return HttpResponseNotFound() return HttpResponseNotFound()
status_count = models.Status.objects.count() status_count = models.Status.objects.filter(user__local=True).count()
user_count = models.User.objects.count() user_count = models.User.objects.count()
return JsonResponse({ return JsonResponse({
"version": "2.0", "version": "2.0",
@ -66,13 +67,12 @@ def nodeinfo(request):
"activeMonth": user_count, # TODO "activeMonth": user_count, # TODO
"activeHalfyear": user_count, # TODO "activeHalfyear": user_count, # TODO
}, },
"localPosts": status_count, # TODO: mark local "localPosts": status_count,
}, },
"openRegistrations": True, "openRegistrations": True,
}) })
def instance_info(request): def instance_info(request):
''' what this place is TODO: should be settable/editable ''' ''' what this place is TODO: should be settable/editable '''
if request.method != 'GET': if request.method != 'GET':
@ -98,3 +98,13 @@ def instance_info(request):
'registrations': True, 'registrations': True,
'approval_required': False, 'approval_required': False,
}) })
def peers(request):
''' list of federated servers this instance connects with '''
if request.method != 'GET':
return HttpResponseNotFound()
# TODO
return JsonResponse([])

View file

@ -8,9 +8,7 @@ https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
""" """
import os import os
from environs import Env from environs import Env
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
Env.read_env() Env.read_env()