mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-01-03 05:48:44 +00:00
Adds status views
This commit is contained in:
parent
85d01d5df0
commit
4ec64c02f4
10 changed files with 269 additions and 314 deletions
|
@ -213,111 +213,6 @@ def handle_imported_book(user, item, include_reviews, privacy):
|
||||||
broadcast(user, review.to_create_activity(user), privacy=privacy)
|
broadcast(user, review.to_create_activity(user), privacy=privacy)
|
||||||
|
|
||||||
|
|
||||||
def handle_delete_status(user, status):
|
|
||||||
''' delete a status and broadcast deletion to other servers '''
|
|
||||||
delete_status(status)
|
|
||||||
broadcast(user, status.to_delete_activity(user))
|
|
||||||
|
|
||||||
|
|
||||||
def handle_status(user, form):
|
|
||||||
''' generic handler for statuses '''
|
|
||||||
status = form.save(commit=False)
|
|
||||||
if not status.sensitive and status.content_warning:
|
|
||||||
# the cw text field remains populated when you click "remove"
|
|
||||||
status.content_warning = None
|
|
||||||
status.save()
|
|
||||||
|
|
||||||
# inspect the text for user tags
|
|
||||||
content = status.content
|
|
||||||
for (mention_text, mention_user) in find_mentions(content):
|
|
||||||
# add them to status mentions fk
|
|
||||||
status.mention_users.add(mention_user)
|
|
||||||
|
|
||||||
# turn the mention into a link
|
|
||||||
content = re.sub(
|
|
||||||
r'%s([^@]|$)' % mention_text,
|
|
||||||
r'<a href="%s">%s</a>\g<1>' % \
|
|
||||||
(mention_user.remote_id, mention_text),
|
|
||||||
content)
|
|
||||||
|
|
||||||
# add reply parent to mentions and notify
|
|
||||||
if status.reply_parent:
|
|
||||||
status.mention_users.add(status.reply_parent.user)
|
|
||||||
for mention_user in status.reply_parent.mention_users.all():
|
|
||||||
status.mention_users.add(mention_user)
|
|
||||||
|
|
||||||
if status.reply_parent.user.local:
|
|
||||||
create_notification(
|
|
||||||
status.reply_parent.user,
|
|
||||||
'REPLY',
|
|
||||||
related_user=user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
# deduplicate mentions
|
|
||||||
status.mention_users.set(set(status.mention_users.all()))
|
|
||||||
# create mention notifications
|
|
||||||
for mention_user in status.mention_users.all():
|
|
||||||
if status.reply_parent and mention_user == status.reply_parent.user:
|
|
||||||
continue
|
|
||||||
if mention_user.local:
|
|
||||||
create_notification(
|
|
||||||
mention_user,
|
|
||||||
'MENTION',
|
|
||||||
related_user=user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
# don't apply formatting to generated notes
|
|
||||||
if not isinstance(status, models.GeneratedNote):
|
|
||||||
status.content = to_markdown(content)
|
|
||||||
# do apply formatting to quotes
|
|
||||||
if hasattr(status, 'quote'):
|
|
||||||
status.quote = to_markdown(status.quote)
|
|
||||||
|
|
||||||
status.save()
|
|
||||||
|
|
||||||
broadcast(user, status.to_create_activity(user), software='bookwyrm')
|
|
||||||
|
|
||||||
# re-format the activity for non-bookwyrm servers
|
|
||||||
remote_activity = status.to_create_activity(user, pure=True)
|
|
||||||
broadcast(user, remote_activity, software='other')
|
|
||||||
|
|
||||||
|
|
||||||
def find_mentions(content):
|
|
||||||
''' detect @mentions in raw status content '''
|
|
||||||
for match in re.finditer(regex.strict_username, content):
|
|
||||||
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
|
|
||||||
yield (match.group(), mention_user)
|
|
||||||
|
|
||||||
|
|
||||||
def format_links(content):
|
|
||||||
''' detect and format links '''
|
|
||||||
return re.sub(
|
|
||||||
r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
|
|
||||||
regex.domain,
|
|
||||||
r'\g<1><a href="\g<2>">\g<3></a>',
|
|
||||||
content)
|
|
||||||
|
|
||||||
def to_markdown(content):
|
|
||||||
''' catch links and convert to markdown '''
|
|
||||||
content = format_links(content)
|
|
||||||
content = markdown(content)
|
|
||||||
# sanitize resulting html
|
|
||||||
sanitizer = InputHtmlParser()
|
|
||||||
sanitizer.feed(content)
|
|
||||||
return sanitizer.get_output()
|
|
||||||
|
|
||||||
|
|
||||||
def handle_favorite(user, status):
|
def handle_favorite(user, status):
|
||||||
''' a user likes a status '''
|
''' a user likes a status '''
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<form class="toggle-content hidden tab-option-{{ book.id }}" name="{{ type }}" action="/{{ type }}" method="post" id="tab-{{ type }}-{{ book.id }}">
|
<form class="toggle-content hidden tab-option-{{ book.id }}" name="{{ type }}" action="/post/{{ type }}" method="post" id="tab-{{ type }}-{{ book.id }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
|
|
|
@ -49,18 +49,6 @@ class ViewActions(TestCase):
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
|
||||||
def test_edit_user(self):
|
|
||||||
''' use a form to update a user '''
|
|
||||||
form = forms.EditUserForm(instance=self.local_user)
|
|
||||||
form.data['name'] = 'New Name'
|
|
||||||
request = self.factory.post('', form.data)
|
|
||||||
request.user = self.local_user
|
|
||||||
|
|
||||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
|
||||||
actions.edit_profile(request)
|
|
||||||
self.assertEqual(self.local_user.name, 'New Name')
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_book(self):
|
def test_edit_book(self):
|
||||||
''' lets a user edit a book '''
|
''' lets a user edit a book '''
|
||||||
self.local_user.groups.add(self.group)
|
self.local_user.groups.add(self.group)
|
||||||
|
|
|
@ -220,57 +220,6 @@ class Views(TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.context_data['user_results'][0], self.local_user)
|
response.context_data['user_results'][0], self.local_user)
|
||||||
|
|
||||||
|
|
||||||
def test_status_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
content='hi', user=self.local_user)
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.status_page(request, 'mouse', status.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'status.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.status_page(request, 'mouse', status.id)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_replies_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
status = models.Status.objects.create(
|
|
||||||
content='hi', user=self.local_user)
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.replies_page(request, 'mouse', status.id)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'status.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.replies_page(request, 'mouse', status.id)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_profile_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
result = views.edit_profile_page(request)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'edit_user.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_book_page(self):
|
def test_book_page(self):
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
''' there are so many views, this just makes sure it LOADS '''
|
||||||
request = self.factory.get('')
|
request = self.factory.get('')
|
||||||
|
|
|
@ -39,8 +39,6 @@ urlpatterns = [
|
||||||
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),
|
re_path(r'^api/v1/instance/peers/?$', wellknown.peers),
|
||||||
# TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta),
|
|
||||||
# TODO: robots.txt
|
|
||||||
|
|
||||||
# authentication
|
# authentication
|
||||||
re_path(r'^login/?$', views.Login.as_view()),
|
re_path(r'^login/?$', views.Login.as_view()),
|
||||||
|
@ -60,16 +58,13 @@ urlpatterns = [
|
||||||
path('', views.Home.as_view()),
|
path('', views.Home.as_view()),
|
||||||
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
|
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
|
||||||
re_path(r'^discover/?$', views.Discover.as_view()),
|
re_path(r'^discover/?$', views.Discover.as_view()),
|
||||||
|
|
||||||
re_path(r'^notifications/?$', views.Notifications.as_view()),
|
re_path(r'^notifications/?$', views.Notifications.as_view()),
|
||||||
|
|
||||||
re_path(r'^direct-messages/?$', views.DirectMessage.as_view()),
|
re_path(r'^direct-messages/?$', views.DirectMessage.as_view()),
|
||||||
|
|
||||||
# imports
|
# imports
|
||||||
re_path(r'^import/?$', views.Import.as_view()),
|
re_path(r'^import/?$', views.Import.as_view()),
|
||||||
re_path(r'^import/(\d+)/?$', views.ImportStatus.as_view()),
|
re_path(r'^import/(\d+)/?$', views.ImportStatus.as_view()),
|
||||||
|
|
||||||
|
|
||||||
# users
|
# users
|
||||||
re_path(r'%s/?$' % user_path, views.User.as_view()),
|
re_path(r'%s/?$' % user_path, views.User.as_view()),
|
||||||
re_path(r'%s\.json$' % user_path, views.User.as_view()),
|
re_path(r'%s\.json$' % user_path, views.User.as_view()),
|
||||||
|
@ -79,16 +74,29 @@ urlpatterns = [
|
||||||
re_path(r'^edit-profile/?$', views.EditUser.as_view()),
|
re_path(r'^edit-profile/?$', views.EditUser.as_view()),
|
||||||
|
|
||||||
# statuses
|
# statuses
|
||||||
re_path(r'%s(.json)?/?$' % status_path, vviews.status_page),
|
re_path(r'%s(.json)?/?$' % status_path, views.Status.as_view()),
|
||||||
re_path(r'%s/activity/?$' % status_path, vviews.status_page),
|
re_path(r'%s/activity/?$' % status_path, views.Status.as_view()),
|
||||||
re_path(r'%s/replies(.json)?/?$' % status_path, vviews.replies_page),
|
re_path(r'%s/replies(.json)?/?$' % status_path, views.Replies.as_view()),
|
||||||
|
re_path(r'^post/(?P<status_type>\w+)/?$', views.CreateStatus.as_view()),
|
||||||
|
re_path(r'^delete-status/(?P<status_id>\d+)/?$',
|
||||||
|
views.DeleteStatus.as_view()),
|
||||||
|
|
||||||
|
|
||||||
|
re_path(r'^tag/?$', actions.tag),
|
||||||
|
re_path(r'^untag/?$', actions.untag),
|
||||||
# books
|
# books
|
||||||
re_path(r'%s(.json)?/?$' % book_path, vviews.book_page),
|
re_path(r'%s(.json)?/?$' % book_path, vviews.book_page),
|
||||||
re_path(r'%s/edit/?$' % book_path, vviews.edit_book_page),
|
re_path(r'%s/edit/?$' % book_path, vviews.edit_book_page),
|
||||||
re_path(r'^author/(?P<author_id>[\w\-]+)/edit/?$', vviews.edit_author_page),
|
re_path(r'^author/(?P<author_id>[\w\-]+)/edit/?$', vviews.edit_author_page),
|
||||||
re_path(r'%s/editions(.json)?/?$' % book_path, vviews.editions_page),
|
re_path(r'%s/editions(.json)?/?$' % book_path, vviews.editions_page),
|
||||||
|
|
||||||
|
# interact
|
||||||
|
re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite),
|
||||||
|
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
|
||||||
|
re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost),
|
||||||
|
re_path(r'^unboost/(?P<status_id>\d+)/?$', actions.unboost),
|
||||||
|
|
||||||
|
|
||||||
re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', vviews.author_page),
|
re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', vviews.author_page),
|
||||||
re_path(r'^tag/(?P<tag_id>.+)\.json/?$', vviews.tag_page),
|
re_path(r'^tag/(?P<tag_id>.+)\.json/?$', vviews.tag_page),
|
||||||
re_path(r'^tag/(?P<tag_id>.+)/?$', vviews.tag_page),
|
re_path(r'^tag/(?P<tag_id>.+)/?$', vviews.tag_page),
|
||||||
|
@ -112,21 +120,6 @@ urlpatterns = [
|
||||||
re_path(r'^delete-readthrough/?$', actions.delete_readthrough),
|
re_path(r'^delete-readthrough/?$', actions.delete_readthrough),
|
||||||
re_path(r'^create-readthrough/?$', actions.create_readthrough),
|
re_path(r'^create-readthrough/?$', actions.create_readthrough),
|
||||||
|
|
||||||
re_path(r'^rate/?$', actions.rate),
|
|
||||||
re_path(r'^review/?$', actions.review),
|
|
||||||
re_path(r'^quote/?$', actions.quotate),
|
|
||||||
re_path(r'^comment/?$', actions.comment),
|
|
||||||
re_path(r'^tag/?$', actions.tag),
|
|
||||||
re_path(r'^untag/?$', actions.untag),
|
|
||||||
re_path(r'^reply/?$', actions.reply),
|
|
||||||
|
|
||||||
re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite),
|
|
||||||
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
|
|
||||||
re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost),
|
|
||||||
re_path(r'^unboost/(?P<status_id>\d+)/?$', actions.unboost),
|
|
||||||
|
|
||||||
re_path(r'^delete-status/(?P<status_id>\d+)/?$', actions.delete_status),
|
|
||||||
|
|
||||||
re_path(r'^create-shelf/?$', actions.create_shelf),
|
re_path(r'^create-shelf/?$', actions.create_shelf),
|
||||||
re_path(r'^edit-shelf/(?P<shelf_id>\d+)?$', actions.edit_shelf),
|
re_path(r'^edit-shelf/(?P<shelf_id>\d+)?$', actions.edit_shelf),
|
||||||
re_path(r'^delete-shelf/(?P<shelf_id>\d+)?$', actions.delete_shelf),
|
re_path(r'^delete-shelf/(?P<shelf_id>\d+)?$', actions.delete_shelf),
|
||||||
|
@ -139,7 +132,4 @@ urlpatterns = [
|
||||||
re_path(r'^unfollow/?$', actions.unfollow),
|
re_path(r'^unfollow/?$', actions.unfollow),
|
||||||
re_path(r'^accept-follow-request/?$', actions.accept_follow_request),
|
re_path(r'^accept-follow-request/?$', actions.accept_follow_request),
|
||||||
re_path(r'^delete-follow-request/?$', actions.delete_follow_request),
|
re_path(r'^delete-follow-request/?$', actions.delete_follow_request),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
|
@ -337,55 +337,6 @@ def create_readthrough(request):
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def rate(request):
|
|
||||||
''' just a star rating for a book '''
|
|
||||||
form = forms.RatingForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def review(request):
|
|
||||||
''' create a book review '''
|
|
||||||
form = forms.ReviewForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def quotate(request):
|
|
||||||
''' create a book quotation '''
|
|
||||||
form = forms.QuotationForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def comment(request):
|
|
||||||
''' create a book comment '''
|
|
||||||
form = forms.CommentForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def reply(request):
|
|
||||||
''' respond to a book review '''
|
|
||||||
form = forms.ReplyForm(request.POST)
|
|
||||||
return handle_status(request, form)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_status(request, form):
|
|
||||||
''' all the "create a status" functions are the same '''
|
|
||||||
if not form.is_valid():
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
outgoing.handle_status(request.user, form)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def tag(request):
|
def tag(request):
|
||||||
|
@ -463,21 +414,6 @@ def unboost(request, status_id):
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@require_POST
|
|
||||||
def delete_status(request, status_id):
|
|
||||||
''' delete and tombstone a status '''
|
|
||||||
status = get_object_or_404(models.Status, id=status_id)
|
|
||||||
|
|
||||||
# don't let people delete other people's statuses
|
|
||||||
if status.user != request.user:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
# perform deletion
|
|
||||||
outgoing.handle_delete_status(request.user, status)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def follow(request):
|
def follow(request):
|
||||||
|
|
|
@ -7,3 +7,4 @@ from .notifications import Notifications
|
||||||
from .direct_message import DirectMessage
|
from .direct_message import DirectMessage
|
||||||
from .import_data import Import, ImportStatus
|
from .import_data import Import, ImportStatus
|
||||||
from .user import User, EditUser, Followers, Following
|
from .user import User, EditUser, Followers, Following
|
||||||
|
from .status import Status, Replies, CreateStatus, DeleteStatus
|
||||||
|
|
|
@ -17,6 +17,27 @@ def is_api_request(request):
|
||||||
request.path[-5:] == '.json'
|
request.path[-5:] == '.json'
|
||||||
|
|
||||||
|
|
||||||
|
def is_bookworm_request(request):
|
||||||
|
''' check if the request is coming from another bookworm instance '''
|
||||||
|
user_agent = request.headers.get('User-Agent')
|
||||||
|
if user_agent is None or \
|
||||||
|
re.search(regex.bookwyrm_user_agent, user_agent) is None:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def status_visible_to_user(viewer, status):
|
||||||
|
''' is a user authorized to view a status? '''
|
||||||
|
if viewer == status.user or status.privacy in ['public', 'unlisted']:
|
||||||
|
return True
|
||||||
|
if status.privacy == 'followers' and \
|
||||||
|
status.user.followers.filter(id=viewer.id).first():
|
||||||
|
return True
|
||||||
|
if status.privacy == 'direct' and \
|
||||||
|
status.mention_users.filter(id=viewer.id).first():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def get_activity_feed(
|
def get_activity_feed(
|
||||||
user, privacy, local_only=False, following_only=False,
|
user, privacy, local_only=False, following_only=False,
|
||||||
queryset=models.Status.objects):
|
queryset=models.Status.objects):
|
||||||
|
@ -73,3 +94,40 @@ def get_activity_feed(
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def handle_remote_webfinger(query):
|
||||||
|
''' webfingerin' other servers '''
|
||||||
|
user = None
|
||||||
|
|
||||||
|
# usernames could be @user@domain or user@domain
|
||||||
|
if not query:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if query[0] == '@':
|
||||||
|
query = query[1:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
domain = query.split('@')[1]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = models.User.objects.get(username=query)
|
||||||
|
except models.User.DoesNotExist:
|
||||||
|
url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \
|
||||||
|
(domain, query)
|
||||||
|
try:
|
||||||
|
data = get_data(url)
|
||||||
|
except (ConnectorException, HTTPError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
for link in data.get('links'):
|
||||||
|
if link.get('rel') == 'self':
|
||||||
|
try:
|
||||||
|
user = activitypub.resolve_remote_id(
|
||||||
|
models.User, link['href']
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
193
bookwyrm/views/status.py
Normal file
193
bookwyrm/views/status.py
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
''' non-interactive pages '''
|
||||||
|
import re
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
from markdown import markdown
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
from bookwyrm.broadcast import broadcast
|
||||||
|
from bookwyrm.sanitize_html import InputHtmlParser
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
from bookwyrm.status import create_notification, delete_status
|
||||||
|
from bookwyrm.utils import regex
|
||||||
|
from .helpers import get_user_from_username, handle_remote_webfinger
|
||||||
|
from .helpers import is_api_request, is_bookworm_request, status_visible_to_user
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Status(View):
|
||||||
|
''' the view for *posting* '''
|
||||||
|
def get(self, request, username, status_id):
|
||||||
|
''' display a particular status (and replies, etc) '''
|
||||||
|
try:
|
||||||
|
user = get_user_from_username(username)
|
||||||
|
status = models.Status.objects.select_subclasses().get(id=status_id)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
# the url should have the poster's username in it
|
||||||
|
if user != status.user:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
# make sure the user is authorized to see the status
|
||||||
|
if not status_visible_to_user(request.user, status):
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
return ActivitypubResponse(
|
||||||
|
status.to_activity(pure=not is_bookworm_request(request)))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': 'Status by %s' % user.username,
|
||||||
|
'status': status,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'status.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class CreateStatus(View):
|
||||||
|
''' get posting '''
|
||||||
|
def post(self, request, status_type):
|
||||||
|
''' create status of whatever type '''
|
||||||
|
if status_type not in models.status_models:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
form = forms.get_attr(status_type)(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
status = form.save(commit=False)
|
||||||
|
if not status.sensitive and status.content_warning:
|
||||||
|
# the cw text field remains populated when you click "remove"
|
||||||
|
status.content_warning = None
|
||||||
|
status.save()
|
||||||
|
|
||||||
|
# inspect the text for user tags
|
||||||
|
content = status.content
|
||||||
|
for (mention_text, mention_user) in find_mentions(content):
|
||||||
|
# add them to status mentions fk
|
||||||
|
status.mention_users.add(mention_user)
|
||||||
|
|
||||||
|
# turn the mention into a link
|
||||||
|
content = re.sub(
|
||||||
|
r'%s([^@]|$)' % mention_text,
|
||||||
|
r'<a href="%s">%s</a>\g<1>' % \
|
||||||
|
(mention_user.remote_id, mention_text),
|
||||||
|
content)
|
||||||
|
|
||||||
|
# add reply parent to mentions and notify
|
||||||
|
if status.reply_parent:
|
||||||
|
status.mention_users.add(status.reply_parent.user)
|
||||||
|
for mention_user in status.reply_parent.mention_users.all():
|
||||||
|
status.mention_users.add(mention_user)
|
||||||
|
|
||||||
|
if status.reply_parent.user.local:
|
||||||
|
create_notification(
|
||||||
|
status.reply_parent.user,
|
||||||
|
'REPLY',
|
||||||
|
related_user=request.user,
|
||||||
|
related_status=status
|
||||||
|
)
|
||||||
|
|
||||||
|
# deduplicate mentions
|
||||||
|
status.mention_users.set(set(status.mention_users.all()))
|
||||||
|
# create mention notifications
|
||||||
|
for mention_user in status.mention_users.all():
|
||||||
|
if status.reply_parent and mention_user == status.reply_parent.user:
|
||||||
|
continue
|
||||||
|
if mention_user.local:
|
||||||
|
create_notification(
|
||||||
|
mention_user,
|
||||||
|
'MENTION',
|
||||||
|
related_user=request.user,
|
||||||
|
related_status=status
|
||||||
|
)
|
||||||
|
|
||||||
|
# don't apply formatting to generated notes
|
||||||
|
if not isinstance(status, models.GeneratedNote):
|
||||||
|
status.content = to_markdown(content)
|
||||||
|
# do apply formatting to quotes
|
||||||
|
if hasattr(status, 'quote'):
|
||||||
|
status.quote = to_markdown(status.quote)
|
||||||
|
|
||||||
|
status.save()
|
||||||
|
|
||||||
|
broadcast(
|
||||||
|
request.user,
|
||||||
|
status.to_create_activity(request.user),
|
||||||
|
software='bookwyrm')
|
||||||
|
|
||||||
|
# re-format the activity for non-bookwyrm servers
|
||||||
|
remote_activity = status.to_create_activity(request.user, pure=True)
|
||||||
|
broadcast(request.user, remote_activity, software='other')
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteStatus(View):
|
||||||
|
''' tombstone that bad boy '''
|
||||||
|
def post(self, request, status_id):
|
||||||
|
''' delete and tombstone a status '''
|
||||||
|
status = get_object_or_404(models.Status, id=status_id)
|
||||||
|
|
||||||
|
# don't let people delete other people's statuses
|
||||||
|
if status.user != request.user:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
# perform deletion
|
||||||
|
delete_status(status)
|
||||||
|
broadcast(request.user, status.to_delete_activity(request.user))
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
class Replies(View):
|
||||||
|
''' replies page (a json view of status) '''
|
||||||
|
def get(self, request, username, status_id):
|
||||||
|
''' ordered collection of replies to a status '''
|
||||||
|
# the html view is the same as Status
|
||||||
|
if not is_api_request(request):
|
||||||
|
status_view = Status.as_view()
|
||||||
|
return status_view(request, username, status_id)
|
||||||
|
|
||||||
|
# the json view is different than Status
|
||||||
|
status = models.Status.objects.get(id=status_id)
|
||||||
|
if status.user.localname != username:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
return ActivitypubResponse(status.to_replies(**request.GET))
|
||||||
|
|
||||||
|
def find_mentions(content):
|
||||||
|
''' detect @mentions in raw status content '''
|
||||||
|
for match in re.finditer(regex.strict_username, content):
|
||||||
|
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
|
||||||
|
yield (match.group(), mention_user)
|
||||||
|
|
||||||
|
|
||||||
|
def format_links(content):
|
||||||
|
''' detect and format links '''
|
||||||
|
return re.sub(
|
||||||
|
r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
|
||||||
|
regex.domain,
|
||||||
|
r'\g<1><a href="\g<2>">\g<3></a>',
|
||||||
|
content)
|
||||||
|
|
||||||
|
def to_markdown(content):
|
||||||
|
''' catch links and convert to markdown '''
|
||||||
|
content = format_links(content)
|
||||||
|
content = markdown(content)
|
||||||
|
# sanitize resulting html
|
||||||
|
sanitizer = InputHtmlParser()
|
||||||
|
sanitizer.feed(content)
|
||||||
|
return sanitizer.get_output()
|
|
@ -157,61 +157,6 @@ def search(request):
|
||||||
return TemplateResponse(request, 'search_results.html', data)
|
return TemplateResponse(request, 'search_results.html', data)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_GET
|
|
||||||
def status_page(request, username, status_id):
|
|
||||||
''' display a particular status (and replies, etc) '''
|
|
||||||
try:
|
|
||||||
user = get_user_from_username(username)
|
|
||||||
status = models.Status.objects.select_subclasses().get(id=status_id)
|
|
||||||
except ValueError:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
# the url should have the poster's username in it
|
|
||||||
if user != status.user:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
# make sure the user is authorized to see the status
|
|
||||||
if not status_visible_to_user(request.user, status):
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
return ActivitypubResponse(
|
|
||||||
status.to_activity(pure=not is_bookworm_request(request)))
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'title': 'Status by %s' % user.username,
|
|
||||||
'status': status,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'status.html', data)
|
|
||||||
|
|
||||||
|
|
||||||
def status_visible_to_user(viewer, status):
|
|
||||||
''' is a user authorized to view a status? '''
|
|
||||||
if viewer == status.user or status.privacy in ['public', 'unlisted']:
|
|
||||||
return True
|
|
||||||
if status.privacy == 'followers' and \
|
|
||||||
status.user.followers.filter(id=viewer.id).first():
|
|
||||||
return True
|
|
||||||
if status.privacy == 'direct' and \
|
|
||||||
status.mention_users.filter(id=viewer.id).first():
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_GET
|
|
||||||
def replies_page(request, username, status_id):
|
|
||||||
''' ordered collection of replies to a status '''
|
|
||||||
if not is_api_request(request):
|
|
||||||
return status_page(request, username, status_id)
|
|
||||||
|
|
||||||
status = models.Status.objects.get(id=status_id)
|
|
||||||
if status.user.localname != username:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
return ActivitypubResponse(status.to_replies(**request.GET))
|
|
||||||
|
|
||||||
@require_GET
|
@require_GET
|
||||||
def book_page(request, book_id):
|
def book_page(request, book_id):
|
||||||
''' info about a book '''
|
''' info about a book '''
|
||||||
|
|
Loading…
Reference in a new issue