Mention users implicitly (without needing @mentions)

This commit is contained in:
Mouse Reeve 2021-01-01 09:35:14 -08:00
parent b3449040d9
commit bb5ae850e1
3 changed files with 149 additions and 42 deletions

View file

@ -220,8 +220,65 @@ def handle_status(user, form):
status.save()
# inspect the text for user tags
matches = []
for match in re.finditer(regex.strict_username, status.content):
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
@ -232,44 +289,7 @@ def handle_status(user, form):
if not mention_user:
# we can ignore users we don't know about
continue
matches.append((match.group(), mention_user.remote_id))
# 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
)
# add mentions
content = status.content
for (username, url) in matches:
content = re.sub(
r'%s([^@])' % username,
r'<a href="%s">%s</a>\g<1>' % (url, username),
content)
if not isinstance(status, models.GeneratedNote):
status.content = to_markdown(content)
if hasattr(status, 'quote'):
status.quote = to_markdown(status.quote)
status.save()
# notify reply parent or tagged users
if status.reply_parent and status.reply_parent.user.local:
create_notification(
status.reply_parent.user,
'REPLY',
related_user=user,
related_status=status
)
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')
yield (match.group(), mention_user)
def to_markdown(content):

View file

@ -10,7 +10,7 @@
{% include 'snippets/content_warning_field.html' with parent_status=status %}
<label for="id_content_{{ status.id }}-{{ uuid }}" class="is-sr-only">Reply</label>
<div class="field">
<textarea class="textarea" name="content" placeholder="Leave a comment..." id="id_content_{{ status.id }}-{{ uuid }}" required="true">{{ status|mentions:request.user }}</textarea>
<textarea class="textarea" name="content" placeholder="Leave a comment..." id="id_content_{{ status.id }}-{{ uuid }}" required="true"></textarea>
</div>
</div>
<div class="column is-narrow">

View file

@ -8,10 +8,11 @@ from django.test import TestCase
from django.test.client import RequestFactory
import responses
from bookwyrm import models, outgoing
from bookwyrm import forms, models, outgoing
from bookwyrm.settings import DOMAIN
# pylint: disable=too-many-public-methods
class Outgoing(TestCase):
''' sends out activities '''
def setUp(self):
@ -255,3 +256,89 @@ class Outgoing(TestCase):
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_unshelve(self.local_user, self.book, self.shelf)
self.assertEqual(self.shelf.books.count(), 0)
def test_handle_status(self):
''' create a status '''
form = forms.CommentForm({
'content': 'hi',
'user': self.local_user.id,
'book': self.book.id,
'privacy': 'public',
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(self.local_user, form)
status = models.Comment.objects.get()
self.assertEqual(status.content, '<p>hi</p>')
self.assertEqual(status.user, self.local_user)
self.assertEqual(status.book, self.book)
def test_handle_status_reply(self):
''' create a status in reply to an existing status '''
user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'password', local=True)
parent = models.Status.objects.create(
content='parent status', user=self.local_user)
form = forms.ReplyForm({
'content': 'hi',
'user': user.id,
'reply_parent': parent.id,
'privacy': 'public',
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(user, form)
status = models.Status.objects.get(user=user)
self.assertEqual(status.content, '<p>hi</p>')
self.assertEqual(status.user, user)
self.assertEqual(
models.Notification.objects.get().user, self.local_user)
def test_handle_status_mentions(self):
''' @mention a user in a post '''
user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'password', local=True)
form = forms.CommentForm({
'content': 'hi @rat',
'user': self.local_user.id,
'book': self.book.id,
'privacy': 'public',
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(self.local_user, form)
status = models.Status.objects.get()
self.assertEqual(
status.content,
'<p>hi <a href="%s">@rat</a></p>' % user.remote_id)
self.assertEqual(list(status.mention_users.all()), [user])
self.assertEqual(models.Notification.objects.get().user, user)
def test_handle_status_reply_with_mentions(self):
''' reply to a post with an @mention'ed user '''
user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'password', local=True)
form = forms.CommentForm({
'content': 'hi @rat@example.com',
'user': self.local_user.id,
'book': self.book.id,
'privacy': 'public',
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(self.local_user, form)
status = models.Status.objects.get()
form = forms.ReplyForm({
'content': 'right',
'user': user,
'privacy': 'public',
'reply_parent': status.id
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(user, form)
reply = models.Status.replies(status).first()
self.assertEqual(reply.content, '<p>right</p>')
self.assertEqual(reply.user, user)
self.assertTrue(self.remote_user in reply.mention_users.all())
self.assertTrue(self.local_user in reply.mention_users.all())