Merge pull request #271 from mouse-reeve/mention_users

Mention users
This commit is contained in:
Mouse Reeve 2020-11-01 11:09:25 -08:00 committed by GitHub
commit 44168e74ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 114 additions and 24 deletions

View file

@ -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

View file

@ -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 '''

View file

@ -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'

View 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'),
),
]

View file

@ -59,7 +59,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@property @property
def ap_tag(self): def ap_tag(self):
''' books or (eventually) users tagged in a post ''' ''' 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(
@ -67,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 = [
@ -286,7 +292,7 @@ class ReadThrough(BookWyrmModel):
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 '''

View file

@ -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,

View file

@ -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

View file

@ -0,0 +1 @@
from .regex import username

5
bookwyrm/utils/regex.py Normal file
View 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

View file

@ -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

View file

@ -1,8 +1,9 @@
''' responds to various requests to /.well-know ''' ''' responds to various requests to /.well-know '''
from datetime import datetime from datetime import datetime
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseNotFound
from django.http import JsonResponse from django.http import JsonResponse
from bookwyrm import models from bookwyrm import models
@ -16,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',