2021-03-08 16:49:10 +00:00
|
|
|
""" helper functions used in various views """
|
2021-01-12 22:43:59 +00:00
|
|
|
import re
|
2021-09-30 17:00:05 +00:00
|
|
|
from datetime import datetime
|
|
|
|
import dateutil.parser
|
|
|
|
import dateutil.tz
|
|
|
|
from dateutil.parser import ParserError
|
|
|
|
|
2021-01-12 22:43:59 +00:00
|
|
|
from requests import HTTPError
|
2021-02-24 19:59:21 +00:00
|
|
|
from django.core.exceptions import FieldError
|
2021-08-08 00:12:09 +00:00
|
|
|
from django.db.models import Q
|
2021-04-30 16:33:36 +00:00
|
|
|
from django.http import Http404
|
2021-01-12 22:43:59 +00:00
|
|
|
|
|
|
|
from bookwyrm import activitypub, models
|
|
|
|
from bookwyrm.connectors import ConnectorException, get_data
|
2021-01-13 19:45:08 +00:00
|
|
|
from bookwyrm.status import create_generated_note
|
2021-01-12 22:43:59 +00:00
|
|
|
from bookwyrm.utils import regex
|
|
|
|
|
2021-01-12 18:44:17 +00:00
|
|
|
|
2021-02-23 20:41:37 +00:00
|
|
|
def get_user_from_username(viewer, username):
|
2021-04-26 16:15:42 +00:00
|
|
|
"""helper function to resolve a localname or a username to a user"""
|
2021-05-23 04:33:56 +00:00
|
|
|
if viewer.is_authenticated and viewer.localname == username:
|
2021-05-23 02:54:50 +00:00
|
|
|
# that's yourself, fool
|
|
|
|
return viewer
|
|
|
|
|
2021-04-30 16:33:36 +00:00
|
|
|
# raises 404 if the user isn't found
|
2021-01-12 20:05:30 +00:00
|
|
|
try:
|
2021-02-23 21:05:43 +00:00
|
|
|
return models.User.viewer_aware_objects(viewer).get(localname=username)
|
2021-01-12 20:05:30 +00:00
|
|
|
except models.User.DoesNotExist:
|
2021-04-30 16:33:36 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
# if the localname didn't match, try the username
|
|
|
|
try:
|
2021-02-23 20:41:37 +00:00
|
|
|
return models.User.viewer_aware_objects(viewer).get(username=username)
|
2021-04-30 16:33:36 +00:00
|
|
|
except models.User.DoesNotExist:
|
|
|
|
raise Http404()
|
2021-01-12 20:05:30 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_api_request(request):
|
2021-04-26 16:15:42 +00:00
|
|
|
"""check whether a request is asking for html or data"""
|
2021-03-29 21:05:58 +00:00
|
|
|
return "json" in request.headers.get("Accept", "") or request.path[-5:] == ".json"
|
2021-01-12 20:05:30 +00:00
|
|
|
|
|
|
|
|
2021-02-23 19:34:15 +00:00
|
|
|
def is_bookwyrm_request(request):
|
2021-04-26 16:15:42 +00:00
|
|
|
"""check if the request is coming from another bookwyrm instance"""
|
2021-03-08 16:49:10 +00:00
|
|
|
user_agent = request.headers.get("User-Agent")
|
2021-06-18 21:12:56 +00:00
|
|
|
if user_agent is None or re.search(regex.BOOKWYRM_USER_AGENT, user_agent) is None:
|
2021-01-12 21:47:00 +00:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2021-02-24 19:35:19 +00:00
|
|
|
def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
|
2021-04-26 16:15:42 +00:00
|
|
|
"""filter objects that have "user" and "privacy" fields"""
|
2021-03-08 16:49:10 +00:00
|
|
|
privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"]
|
2021-03-23 02:17:46 +00:00
|
|
|
# if there'd a deleted field, exclude deleted items
|
|
|
|
try:
|
|
|
|
queryset = queryset.filter(deleted=False)
|
|
|
|
except FieldError:
|
|
|
|
pass
|
2021-02-24 19:35:19 +00:00
|
|
|
|
2021-01-25 00:13:26 +00:00
|
|
|
# exclude blocks from both directions
|
2021-01-31 16:41:11 +00:00
|
|
|
if not viewer.is_anonymous:
|
|
|
|
blocked = models.User.objects.filter(id__in=viewer.blocks.all()).all()
|
2021-03-08 16:49:10 +00:00
|
|
|
queryset = queryset.exclude(Q(user__in=blocked) | Q(user__blocks=viewer))
|
2021-01-25 00:13:26 +00:00
|
|
|
|
2021-01-12 18:44:17 +00:00
|
|
|
# you can't see followers only or direct messages if you're not logged in
|
2021-01-31 16:41:11 +00:00
|
|
|
if viewer.is_anonymous:
|
2021-03-08 16:49:10 +00:00
|
|
|
privacy_levels = [p for p in privacy_levels if not p in ["followers", "direct"]]
|
2021-01-12 18:44:17 +00:00
|
|
|
|
|
|
|
# filter to only privided privacy levels
|
2021-01-31 16:41:11 +00:00
|
|
|
queryset = queryset.filter(privacy__in=privacy_levels)
|
2021-01-12 18:44:17 +00:00
|
|
|
|
|
|
|
# only include statuses the user follows
|
|
|
|
if following_only:
|
|
|
|
queryset = queryset.exclude(
|
2021-03-08 16:49:10 +00:00
|
|
|
~Q( # remove everythign except
|
|
|
|
Q(user__in=viewer.following.all())
|
|
|
|
| Q(user=viewer) # user following
|
|
|
|
| Q(mention_users=viewer) # is self # mentions user
|
2021-01-12 18:44:17 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
# exclude followers-only statuses the user doesn't follow
|
2021-03-08 16:49:10 +00:00
|
|
|
elif "followers" in privacy_levels:
|
2021-01-12 18:44:17 +00:00
|
|
|
queryset = queryset.exclude(
|
2021-03-08 16:49:10 +00:00
|
|
|
~Q( # user isn't following and it isn't their own status
|
2021-09-20 21:29:01 +00:00
|
|
|
Q(user__followers=viewer) | Q(user=viewer)
|
2021-01-12 18:44:17 +00:00
|
|
|
),
|
2021-03-08 16:49:10 +00:00
|
|
|
privacy="followers", # and the status is followers only
|
2021-01-12 18:44:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
# exclude direct messages not intended for the user
|
2021-03-08 16:49:10 +00:00
|
|
|
if "direct" in privacy_levels:
|
2021-02-24 19:59:21 +00:00
|
|
|
try:
|
|
|
|
queryset = queryset.exclude(
|
2021-03-08 16:49:10 +00:00
|
|
|
~Q(Q(user=viewer) | Q(mention_users=viewer)), privacy="direct"
|
2021-02-24 19:59:21 +00:00
|
|
|
)
|
|
|
|
except FieldError:
|
2021-03-08 16:49:10 +00:00
|
|
|
queryset = queryset.exclude(~Q(user=viewer), privacy="direct")
|
2021-02-24 19:59:21 +00:00
|
|
|
|
2021-01-31 16:41:11 +00:00
|
|
|
return queryset
|
|
|
|
|
|
|
|
|
2021-01-12 21:47:00 +00:00
|
|
|
def handle_remote_webfinger(query):
|
2021-04-26 16:15:42 +00:00
|
|
|
"""webfingerin' other servers"""
|
2021-01-12 21:47:00 +00:00
|
|
|
user = None
|
|
|
|
|
|
|
|
# usernames could be @user@domain or user@domain
|
|
|
|
if not query:
|
|
|
|
return None
|
|
|
|
|
2021-03-08 16:49:10 +00:00
|
|
|
if query[0] == "@":
|
2021-01-12 21:47:00 +00:00
|
|
|
query = query[1:]
|
|
|
|
|
|
|
|
try:
|
2021-03-08 16:49:10 +00:00
|
|
|
domain = query.split("@")[1]
|
2021-01-12 21:47:00 +00:00
|
|
|
except IndexError:
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
2021-04-08 16:59:21 +00:00
|
|
|
user = models.User.objects.get(username__iexact=query)
|
2021-01-12 21:47:00 +00:00
|
|
|
except models.User.DoesNotExist:
|
2021-09-18 18:32:00 +00:00
|
|
|
url = f"https://{domain}/.well-known/webfinger?resource=acct:{query}"
|
2021-01-12 21:47:00 +00:00
|
|
|
try:
|
|
|
|
data = get_data(url)
|
|
|
|
except (ConnectorException, HTTPError):
|
|
|
|
return None
|
|
|
|
|
2021-03-08 16:49:10 +00:00
|
|
|
for link in data.get("links"):
|
|
|
|
if link.get("rel") == "self":
|
2021-01-12 21:47:00 +00:00
|
|
|
try:
|
|
|
|
user = activitypub.resolve_remote_id(
|
2021-03-08 16:49:10 +00:00
|
|
|
link["href"], model=models.User
|
2021-01-12 21:47:00 +00:00
|
|
|
)
|
2021-04-07 16:17:04 +00:00
|
|
|
except (KeyError, activitypub.ActivitySerializerError):
|
2021-01-12 21:47:00 +00:00
|
|
|
return None
|
|
|
|
return user
|
2021-01-13 17:42:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_edition(book_id):
|
2021-04-26 16:15:42 +00:00
|
|
|
"""look up a book in the db and return an edition"""
|
2021-01-13 17:42:54 +00:00
|
|
|
book = models.Book.objects.select_subclasses().get(id=book_id)
|
|
|
|
if isinstance(book, models.Work):
|
2021-04-28 22:19:24 +00:00
|
|
|
book = book.default_edition
|
2021-01-13 17:42:54 +00:00
|
|
|
return book
|
|
|
|
|
|
|
|
|
2021-01-13 19:45:08 +00:00
|
|
|
def handle_reading_status(user, shelf, book, privacy):
|
2021-04-26 16:15:42 +00:00
|
|
|
"""post about a user reading a book"""
|
2021-01-13 19:45:08 +00:00
|
|
|
# tell the world about this cool thing that happened
|
|
|
|
try:
|
|
|
|
message = {
|
2021-03-08 16:49:10 +00:00
|
|
|
"to-read": "wants to read",
|
|
|
|
"reading": "started reading",
|
|
|
|
"read": "finished reading",
|
2021-01-13 19:45:08 +00:00
|
|
|
}[shelf.identifier]
|
|
|
|
except KeyError:
|
|
|
|
# it's a non-standard shelf, don't worry about it
|
|
|
|
return
|
|
|
|
|
2021-03-08 16:49:10 +00:00
|
|
|
status = create_generated_note(user, message, mention_books=[book], privacy=privacy)
|
2021-01-13 19:45:08 +00:00
|
|
|
status.save()
|
|
|
|
|
2021-01-26 16:31:55 +00:00
|
|
|
|
|
|
|
def is_blocked(viewer, user):
|
2021-04-26 16:15:42 +00:00
|
|
|
"""is this viewer blocked by the user?"""
|
2021-01-26 16:31:55 +00:00
|
|
|
if viewer.is_authenticated and viewer in user.blocks.all():
|
|
|
|
return True
|
|
|
|
return False
|
2021-03-21 02:14:41 +00:00
|
|
|
|
|
|
|
|
2021-08-07 18:15:02 +00:00
|
|
|
def get_landing_books():
|
|
|
|
"""list of books for the landing page"""
|
2021-08-07 23:37:51 +00:00
|
|
|
|
2021-08-08 00:12:09 +00:00
|
|
|
return list(
|
|
|
|
set(
|
|
|
|
models.Edition.objects.filter(
|
|
|
|
review__published_date__isnull=False,
|
|
|
|
review__deleted=False,
|
|
|
|
review__user__local=True,
|
|
|
|
review__privacy__in=["public", "unlisted"],
|
|
|
|
)
|
|
|
|
.exclude(cover__exact="")
|
|
|
|
.distinct()
|
|
|
|
.order_by("-review__published_date")[:6]
|
2021-03-21 02:14:41 +00:00
|
|
|
)
|
2021-05-22 16:55:38 +00:00
|
|
|
)
|
2021-09-30 17:00:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
|
|
|
|
"""ensures that data is stored consistently in the UTC timezone"""
|
|
|
|
if not date_str:
|
|
|
|
return None
|
|
|
|
user_tz = dateutil.tz.gettz(user.preferred_timezone)
|
|
|
|
date = dateutil.parser.parse(date_str, ignoretz=True)
|
|
|
|
try:
|
|
|
|
return date.replace(tzinfo=user_tz).astimezone(dateutil.tz.UTC)
|
|
|
|
except ParserError:
|
|
|
|
return None
|