1
0
Fork 0

Adds status views

This commit is contained in:
Mouse Reeve 2021-01-12 13:47:00 -08:00
parent 85d01d5df0
commit 4ec64c02f4
10 changed files with 269 additions and 314 deletions

View file

@ -7,3 +7,4 @@ from .notifications import Notifications
from .direct_message import DirectMessage
from .import_data import Import, ImportStatus
from .user import User, EditUser, Followers, Following
from .status import Status, Replies, CreateStatus, DeleteStatus

View file

@ -17,6 +17,27 @@ def is_api_request(request):
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(
user, privacy, local_only=False, following_only=False,
queryset=models.Status.objects):
@ -73,3 +94,40 @@ def get_activity_feed(
pass
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
View 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()