mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-14 12:16:31 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
875b473711
19 changed files with 235 additions and 54 deletions
|
@ -3,8 +3,9 @@ import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .base_activity import ActivityEncoder, Image, PublicKey, Signature
|
from .base_activity import ActivityEncoder, Image, PublicKey, Signature
|
||||||
|
from .base_activity import Link, Mention
|
||||||
from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
|
from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
|
||||||
from .note import Tombstone, Link
|
from .note import Tombstone
|
||||||
from .interaction import Boost, Like
|
from .interaction import Boost, Like
|
||||||
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
||||||
from .person import Person
|
from .person import Person
|
||||||
|
|
|
@ -21,6 +21,19 @@ class Image:
|
||||||
type: str = 'Image'
|
type: str = 'Image'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Link():
|
||||||
|
''' for tagging a book in a status '''
|
||||||
|
href: str
|
||||||
|
name: str
|
||||||
|
type: str = 'Link'
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Mention(Link):
|
||||||
|
''' a subtype of Link for mentioning an actor '''
|
||||||
|
type: str = 'Mention'
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PublicKey:
|
class PublicKey:
|
||||||
''' public key block '''
|
''' public key block '''
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from .base_activity import ActivityObject, Image
|
from .base_activity import ActivityObject, Image, Link
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Tombstone(ActivityObject):
|
class Tombstone(ActivityObject):
|
||||||
|
@ -20,6 +20,7 @@ class Note(ActivityObject):
|
||||||
inReplyTo: str
|
inReplyTo: str
|
||||||
published: str
|
published: str
|
||||||
attributedTo: str
|
attributedTo: str
|
||||||
|
tag: List[Link]
|
||||||
to: List[str]
|
to: List[str]
|
||||||
cc: List[str]
|
cc: List[str]
|
||||||
content: str
|
content: str
|
||||||
|
@ -36,17 +37,9 @@ class Article(Note):
|
||||||
type: str = 'Article'
|
type: str = 'Article'
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Link():
|
|
||||||
''' for tagging a book in a status '''
|
|
||||||
href: str
|
|
||||||
name: str
|
|
||||||
type: str = 'Link'
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class GeneratedNote(Note):
|
class GeneratedNote(Note):
|
||||||
''' just a re-typed note '''
|
''' just a re-typed note '''
|
||||||
tag: List[Link]
|
|
||||||
type: str = 'GeneratedNote'
|
type: str = 'GeneratedNote'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -230,7 +230,7 @@ def handle_create(activity):
|
||||||
if hasattr(activity, 'inReplyToBook'):
|
if hasattr(activity, 'inReplyToBook'):
|
||||||
book_urls.append(activity.inReplyToBook)
|
book_urls.append(activity.inReplyToBook)
|
||||||
if hasattr(activity, 'tag'):
|
if hasattr(activity, 'tag'):
|
||||||
book_urls += [t.href for t in activity.tag if t.type == 'Book']
|
book_urls += [t['href'] for t in activity.tag if t['type'] == 'Book']
|
||||||
for remote_id in book_urls:
|
for remote_id in book_urls:
|
||||||
books_manager.get_or_create_book(remote_id)
|
books_manager.get_or_create_book(remote_id)
|
||||||
|
|
||||||
|
|
26
bookwyrm/migrations/0063_auto_20201101_1758.py
Normal file
26
bookwyrm/migrations/0063_auto_20201101_1758.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 3.0.7 on 2020-11-01 17:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bookwyrm', '0062_auto_20201031_1936'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name='notification',
|
||||||
|
name='notification_type_valid',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='notification',
|
||||||
|
name='notification_type',
|
||||||
|
field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('MENTION', 'Mention'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import')], max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='notification',
|
||||||
|
constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'MENTION', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT']), name='notification_type_valid'),
|
||||||
|
),
|
||||||
|
]
|
18
bookwyrm/migrations/0063_user_last_active_date.py
Normal file
18
bookwyrm/migrations/0063_user_last_active_date.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.7 on 2020-11-01 17:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bookwyrm', '0062_auto_20201031_1936'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='last_active_date',
|
||||||
|
field=models.DateTimeField(auto_now=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -59,6 +59,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ap_tag(self):
|
def ap_tag(self):
|
||||||
|
''' references to books and/or users '''
|
||||||
|
|
||||||
tags = []
|
tags = []
|
||||||
for book in self.mention_books.all():
|
for book in self.mention_books.all():
|
||||||
tags.append(activitypub.Link(
|
tags.append(activitypub.Link(
|
||||||
|
@ -66,6 +68,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
name=book.title,
|
name=book.title,
|
||||||
type='Book'
|
type='Book'
|
||||||
))
|
))
|
||||||
|
for user in self.mention_users.all():
|
||||||
|
tags.append(activitypub.Mention(
|
||||||
|
href=user.remote_id,
|
||||||
|
name=user.username,
|
||||||
|
))
|
||||||
return tags
|
return tags
|
||||||
|
|
||||||
shared_mappings = [
|
shared_mappings = [
|
||||||
|
@ -117,7 +124,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_activity(self, **kwargs):
|
def to_activity(self, pure=False):
|
||||||
''' return tombstone if the status is deleted '''
|
''' return tombstone if the status is deleted '''
|
||||||
if self.deleted:
|
if self.deleted:
|
||||||
return activitypub.Tombstone(
|
return activitypub.Tombstone(
|
||||||
|
@ -126,7 +133,12 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
deleted=self.deleted_date.isoformat(),
|
deleted=self.deleted_date.isoformat(),
|
||||||
published=self.deleted_date.isoformat()
|
published=self.deleted_date.isoformat()
|
||||||
).serialize()
|
).serialize()
|
||||||
return ActivitypubMixin.to_activity(self, **kwargs)
|
return ActivitypubMixin.to_activity(self, pure=pure)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.user.last_active_date = timezone.now()
|
||||||
|
self.user.save()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class GeneratedNote(Status):
|
class GeneratedNote(Status):
|
||||||
|
@ -227,6 +239,11 @@ class Favorite(ActivitypubMixin, BookWyrmModel):
|
||||||
|
|
||||||
activity_serializer = activitypub.Like
|
activity_serializer = activitypub.Like
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.user.last_active_date = timezone.now()
|
||||||
|
self.user.save()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' can't fav things twice '''
|
''' can't fav things twice '''
|
||||||
|
@ -267,10 +284,15 @@ class ReadThrough(BookWyrmModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True)
|
null=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.user.last_active_date = timezone.now()
|
||||||
|
self.user.save()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
NotificationType = models.TextChoices(
|
NotificationType = models.TextChoices(
|
||||||
'NotificationType',
|
'NotificationType',
|
||||||
'FAVORITE REPLY TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT')
|
'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT')
|
||||||
|
|
||||||
class Notification(BookWyrmModel):
|
class Notification(BookWyrmModel):
|
||||||
''' you've been tagged, liked, followed, etc '''
|
''' you've been tagged, liked, followed, etc '''
|
||||||
|
|
|
@ -69,6 +69,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
remote_id = models.CharField(max_length=255, null=True, unique=True)
|
remote_id = models.CharField(max_length=255, null=True, unique=True)
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
|
last_active_date = models.DateTimeField(auto_now=True)
|
||||||
manually_approves_followers = models.BooleanField(default=False)
|
manually_approves_followers = models.BooleanField(default=False)
|
||||||
|
|
||||||
# ---- activitypub serialization settings for this model ----- #
|
# ---- activitypub serialization settings for this model ----- #
|
||||||
|
@ -167,28 +168,30 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
return activity_object
|
return activity_object
|
||||||
|
|
||||||
|
|
||||||
@receiver(models.signals.pre_save, sender=User)
|
def save(self, *args, **kwargs):
|
||||||
def execute_before_save(sender, instance, *args, **kwargs):
|
|
||||||
''' populate fields for new local users '''
|
''' populate fields for new local users '''
|
||||||
# this user already exists, no need to poplate fields
|
# this user already exists, no need to populate fields
|
||||||
if instance.id:
|
if self.id:
|
||||||
return
|
return
|
||||||
if not instance.local:
|
|
||||||
# we need to generate a username that uses the domain (webfinger format)
|
if not self.local:
|
||||||
actor_parts = urlparse(instance.remote_id)
|
# generate a username that uses the domain (webfinger format)
|
||||||
instance.username = '%s@%s' % (instance.username, actor_parts.netloc)
|
actor_parts = urlparse(self.remote_id)
|
||||||
|
self.username = '%s@%s' % (self.username, actor_parts.netloc)
|
||||||
return
|
return
|
||||||
|
|
||||||
# populate fields for local users
|
# populate fields for local users
|
||||||
instance.remote_id = 'https://%s/user/%s' % (DOMAIN, instance.username)
|
self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.username)
|
||||||
instance.localname = instance.username
|
self.localname = self.username
|
||||||
instance.username = '%s@%s' % (instance.username, DOMAIN)
|
self.username = '%s@%s' % (self.username, DOMAIN)
|
||||||
instance.actor = instance.remote_id
|
self.actor = self.remote_id
|
||||||
instance.inbox = '%s/inbox' % instance.remote_id
|
self.inbox = '%s/inbox' % self.remote_id
|
||||||
instance.shared_inbox = 'https://%s/inbox' % DOMAIN
|
self.shared_inbox = 'https://%s/inbox' % DOMAIN
|
||||||
instance.outbox = '%s/outbox' % instance.remote_id
|
self.outbox = '%s/outbox' % self.remote_id
|
||||||
if not instance.private_key:
|
if not self.private_key:
|
||||||
instance.private_key, instance.public_key = create_key_pair()
|
self.private_key, self.public_key = create_key_pair()
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@receiver(models.signals.post_save, sender=User)
|
@receiver(models.signals.post_save, sender=User)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
''' handles all the activity coming out of the server '''
|
''' handles all the activity coming out of the server '''
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
from django.http import HttpResponseNotFound, JsonResponse
|
from django.http import HttpResponseNotFound, JsonResponse
|
||||||
|
@ -13,6 +14,8 @@ from bookwyrm.status import create_tag, create_notification
|
||||||
from bookwyrm.status import create_generated_note
|
from bookwyrm.status import create_generated_note
|
||||||
from bookwyrm.status import delete_status
|
from bookwyrm.status import delete_status
|
||||||
from bookwyrm.remote_user import get_or_create_remote_user
|
from bookwyrm.remote_user import get_or_create_remote_user
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
from bookwyrm.utils import regex
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
@ -34,13 +37,17 @@ def outbox(request, username):
|
||||||
|
|
||||||
|
|
||||||
def handle_remote_webfinger(query):
|
def handle_remote_webfinger(query):
|
||||||
''' webfingerin' other servers '''
|
''' webfingerin' other servers, username query should be user@domain '''
|
||||||
user = None
|
user = None
|
||||||
domain = query.split('@')[1]
|
try:
|
||||||
|
domain = query.split('@')[2]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = models.User.objects.get(username=query)
|
user = models.User.objects.get(username=query)
|
||||||
except models.User.DoesNotExist:
|
except models.User.DoesNotExist:
|
||||||
url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \
|
url = 'https://%s/.well-known/webfinger?resource=acct:@%s' % \
|
||||||
(domain, query)
|
(domain, query)
|
||||||
try:
|
try:
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
|
@ -55,7 +62,7 @@ def handle_remote_webfinger(query):
|
||||||
user = get_or_create_remote_user(link['href'])
|
user = get_or_create_remote_user(link['href'])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
return [user]
|
return user
|
||||||
|
|
||||||
|
|
||||||
def handle_follow(user, to_follow):
|
def handle_follow(user, to_follow):
|
||||||
|
@ -211,7 +218,36 @@ def handle_status(user, form):
|
||||||
''' generic handler for statuses '''
|
''' generic handler for statuses '''
|
||||||
status = form.save()
|
status = form.save()
|
||||||
|
|
||||||
# notify reply parent or (TODO) tagged users
|
# inspect the text for user tags
|
||||||
|
text = status.content
|
||||||
|
matches = re.finditer(
|
||||||
|
regex.username,
|
||||||
|
text
|
||||||
|
)
|
||||||
|
for match in matches:
|
||||||
|
username = match.group().strip().split('@')[1:]
|
||||||
|
if len(username) == 1:
|
||||||
|
# this looks like a local user (@user), fill in the domain
|
||||||
|
username.append(DOMAIN)
|
||||||
|
username = '@'.join(username)
|
||||||
|
|
||||||
|
mention_user = handle_remote_webfinger(username)
|
||||||
|
if not mention_user:
|
||||||
|
# we can ignore users we don't know about
|
||||||
|
continue
|
||||||
|
# add them to status mentions fk
|
||||||
|
status.mention_users.add(mention_user)
|
||||||
|
# create notification if the mentioned user is local
|
||||||
|
if mention_user.local:
|
||||||
|
create_notification(
|
||||||
|
mention_user,
|
||||||
|
'MENTION',
|
||||||
|
related_user=user,
|
||||||
|
related_status=status
|
||||||
|
)
|
||||||
|
status.save()
|
||||||
|
|
||||||
|
# notify reply parent or tagged users
|
||||||
if status.reply_parent and status.reply_parent.user.local:
|
if status.reply_parent and status.reply_parent.user.local:
|
||||||
create_notification(
|
create_notification(
|
||||||
status.reply_parent.user,
|
status.reply_parent.user,
|
||||||
|
|
|
@ -113,7 +113,7 @@ input.toggle-control:checked ~ .toggle-content {
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
.quote blockquote:after {
|
.quote blockquote:after {
|
||||||
content: "\e903";
|
content: "\e904";
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,10 @@
|
||||||
favorited your
|
favorited your
|
||||||
<a href="{{ notification.related_status.remote_id}}">status</a>
|
<a href="{{ notification.related_status.remote_id}}">status</a>
|
||||||
|
|
||||||
|
{% elif notification.notification_type == 'MENTION' %}
|
||||||
|
mentioned you in a
|
||||||
|
<a href="{{ notification.related_status.remote_id}}">status</a>
|
||||||
|
|
||||||
{% elif notification.notification_type == 'REPLY' %}
|
{% elif notification.notification_type == 'REPLY' %}
|
||||||
<a href="{{ notification.related_status.remote_id}}">replied</a>
|
<a href="{{ notification.related_status.remote_id}}">replied</a>
|
||||||
to your
|
to your
|
||||||
|
|
|
@ -1,2 +1,20 @@
|
||||||
<blockquote class="content">{% if book.description %}{{ book.description }}{% elif book.parent_work.description %}{{ book.parent_work.description }}{% endif %}</blockquote>
|
{% load fr_display %}
|
||||||
|
{% with book|book_description as full %}
|
||||||
|
{% with full|text_overflow as trimmed %}
|
||||||
|
{% if trimmed != full %}
|
||||||
|
<div>
|
||||||
|
<input type="radio" name="show-hide-{{ book.id }}" id="show-{{ book.id }}" class="toggle-control" checked>
|
||||||
|
<blockquote class="content toggle-content hidden">{{ trimmed }}
|
||||||
|
<label class="button is-small" for="hide-{{ book.id }}">show more</label></blockquote>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" name="show-hide-{{ book.id }}" id="hide-{{ book.id }}" class="toggle-control">
|
||||||
|
<blockquote class="content toggle-content hidden">{{ full }}
|
||||||
|
<label class="button is-small" for="show-{{ book.id }}">show less</label></blockquote>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<blockquote class="content">{{ full }}
|
||||||
|
</blockquote>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
|
|
@ -106,6 +106,25 @@ def get_edition_info(book):
|
||||||
return ', '.join(i for i in items if i)
|
return ', '.join(i for i in items if i)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='book_description')
|
||||||
|
def get_book_description(book):
|
||||||
|
''' use the work's text if the book doesn't have it '''
|
||||||
|
return book.description or book.parent_work.description
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='text_overflow')
|
||||||
|
def text_overflow(text):
|
||||||
|
''' dont' let book descriptions run for ages '''
|
||||||
|
char_max = 500
|
||||||
|
if len(text) < char_max:
|
||||||
|
return text
|
||||||
|
|
||||||
|
trimmed = text[:char_max]
|
||||||
|
# go back to the last space
|
||||||
|
trimmed = ' '.join(trimmed.split(' ')[:-1])
|
||||||
|
return trimmed + '...'
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def shelve_button_identifier(context, book):
|
def shelve_button_identifier(context, book):
|
||||||
''' check what shelf a user has a book on, if any '''
|
''' check what shelf a user has a book on, if any '''
|
||||||
|
|
|
@ -17,7 +17,7 @@ status_types = [
|
||||||
'comment',
|
'comment',
|
||||||
'quotation',
|
'quotation',
|
||||||
'boost',
|
'boost',
|
||||||
'generatedstatus'
|
'generatednote'
|
||||||
]
|
]
|
||||||
status_path = r'%s/(%s)/(?P<status_id>\d+)' % \
|
status_path = r'%s/(%s)/(?P<status_id>\d+)' % \
|
||||||
(local_user_path, '|'.join(status_types))
|
(local_user_path, '|'.join(status_types))
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .regex import username
|
5
bookwyrm/utils/regex.py
Normal file
5
bookwyrm/utils/regex.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
''' defining regexes for regularly used concepts '''
|
||||||
|
|
||||||
|
domain = r'[a-z-A-Z0-9_\-]+\.[a-z]+'
|
||||||
|
username = r'@[a-zA-Z_\-\.0-9]+(@%s)?' % domain
|
||||||
|
full_username = r'@[a-zA-Z_\-\.0-9]+@%s' % domain
|
|
@ -4,13 +4,15 @@ from PIL import Image
|
||||||
|
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
from dateutil.parser import ParserError
|
from dateutil.parser import ParserError
|
||||||
|
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import books_manager
|
from bookwyrm import books_manager
|
||||||
from bookwyrm import forms, models, outgoing
|
from bookwyrm import forms, models, outgoing
|
||||||
|
@ -32,7 +34,9 @@ def user_login(request):
|
||||||
password = login_form.data['password']
|
password = login_form.data['password']
|
||||||
user = authenticate(request, username=username, password=password)
|
user = authenticate(request, username=username, password=password)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
|
# successful login
|
||||||
login(request, user)
|
login(request, user)
|
||||||
|
user.last_active_date = timezone.now()
|
||||||
return redirect(request.GET.get('next', '/'))
|
return redirect(request.GET.get('next', '/'))
|
||||||
|
|
||||||
login_form.non_field_errors = 'Username or password are incorrect'
|
login_form.non_field_errors = 'Username or password are incorrect'
|
||||||
|
|
|
@ -16,6 +16,7 @@ from bookwyrm.activitypub import ActivityEncoder
|
||||||
from bookwyrm import forms, models, books_manager
|
from bookwyrm import forms, models, books_manager
|
||||||
from bookwyrm import goodreads_import
|
from bookwyrm import goodreads_import
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
from bookwyrm.utils import regex
|
||||||
|
|
||||||
|
|
||||||
def get_user_from_username(username):
|
def get_user_from_username(username):
|
||||||
|
@ -168,7 +169,7 @@ def search(request):
|
||||||
return JsonResponse([r.__dict__ for r in book_results], safe=False)
|
return JsonResponse([r.__dict__ for r in book_results], safe=False)
|
||||||
|
|
||||||
# use webfinger looks like a mastodon style account@domain.com username
|
# use webfinger looks like a mastodon style account@domain.com username
|
||||||
if re.match(r'\w+@\w+.\w+', query):
|
if re.match(regex.full_username, query):
|
||||||
outgoing.handle_remote_webfinger(query)
|
outgoing.handle_remote_webfinger(query)
|
||||||
|
|
||||||
# do a local user search
|
# do a local user search
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
''' responds to various requests to /.well-know '''
|
''' responds to various requests to /.well-know '''
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from django.http import HttpResponseNotFound
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
@ -13,13 +17,16 @@ def webfinger(request):
|
||||||
|
|
||||||
resource = request.GET.get('resource')
|
resource = request.GET.get('resource')
|
||||||
if not resource and not resource.startswith('acct:'):
|
if not resource and not resource.startswith('acct:'):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseNotFound()
|
||||||
ap_id = resource.replace('acct:', '')
|
|
||||||
user = models.User.objects.filter(username=ap_id).first()
|
username = resource.replace('acct:@', '')
|
||||||
if not user:
|
try:
|
||||||
|
user = models.User.objects.get(username=username)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
return HttpResponseNotFound('No account found')
|
return HttpResponseNotFound('No account found')
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'subject': 'acct:%s' % (user.username),
|
'subject': 'acct:@%s' % (user.username),
|
||||||
'links': [
|
'links': [
|
||||||
{
|
{
|
||||||
'rel': 'self',
|
'rel': 'self',
|
||||||
|
@ -52,6 +59,16 @@ def nodeinfo(request):
|
||||||
|
|
||||||
status_count = models.Status.objects.filter(user__local=True).count()
|
status_count = models.Status.objects.filter(user__local=True).count()
|
||||||
user_count = models.User.objects.count()
|
user_count = models.User.objects.count()
|
||||||
|
|
||||||
|
month_ago = datetime.now() - relativedelta(months=1)
|
||||||
|
last_month_count = models.User.objects.filter(
|
||||||
|
last_active_date__gt=month_ago
|
||||||
|
).count()
|
||||||
|
|
||||||
|
six_months_ago = datetime.now() - relativedelta(months=6)
|
||||||
|
six_month_count = models.User.objects.filter(
|
||||||
|
last_active_date__gt=six_months_ago
|
||||||
|
).count()
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'version': '2.0',
|
'version': '2.0',
|
||||||
'software': {
|
'software': {
|
||||||
|
@ -64,8 +81,8 @@ def nodeinfo(request):
|
||||||
'usage': {
|
'usage': {
|
||||||
'users': {
|
'users': {
|
||||||
'total': user_count,
|
'total': user_count,
|
||||||
'activeMonth': user_count, # TODO
|
'activeMonth': last_month_count,
|
||||||
'activeHalfyear': user_count, # TODO
|
'activeHalfyear': six_month_count,
|
||||||
},
|
},
|
||||||
'localPosts': status_count,
|
'localPosts': status_count,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue