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
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 Tombstone, Link
from .note import Tombstone
from .interaction import Boost, Like
from .ordered_collection import OrderedCollection, OrderedCollectionPage
from .person import Person

View file

@ -21,6 +21,19 @@ class 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
class PublicKey:
''' public key block '''

View file

@ -2,7 +2,7 @@
from dataclasses import dataclass, field
from typing import Dict, List
from .base_activity import ActivityObject, Image
from .base_activity import ActivityObject, Image, Link
@dataclass(init=False)
class Tombstone(ActivityObject):
@ -20,6 +20,7 @@ class Note(ActivityObject):
inReplyTo: str
published: str
attributedTo: str
tag: List[Link]
to: List[str]
cc: List[str]
content: str
@ -36,17 +37,9 @@ class Article(Note):
type: str = 'Article'
@dataclass
class Link():
''' for tagging a book in a status '''
href: str
name: str
type: str = 'Link'
@dataclass(init=False)
class GeneratedNote(Note):
''' just a re-typed note '''
tag: List[Link]
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
def ap_tag(self):
''' books or (eventually) users tagged in a post '''
''' references to books and/or users '''
tags = []
for book in self.mention_books.all():
tags.append(activitypub.Link(
@ -67,6 +68,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
name=book.title,
type='Book'
))
for user in self.mention_users.all():
tags.append(activitypub.Mention(
href=user.remote_id,
name=user.username,
))
return tags
shared_mappings = [
@ -286,7 +292,7 @@ class ReadThrough(BookWyrmModel):
NotificationType = models.TextChoices(
'NotificationType',
'FAVORITE REPLY TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT')
'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT')
class Notification(BookWyrmModel):
''' you've been tagged, liked, followed, etc '''

View file

@ -1,5 +1,6 @@
''' handles all the activity coming out of the server '''
from datetime import datetime
import re
from django.db import IntegrityError, transaction
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 delete_status
from bookwyrm.remote_user import get_or_create_remote_user
from bookwyrm.settings import DOMAIN
from bookwyrm.utils import regex
@csrf_exempt
@ -34,13 +37,17 @@ def outbox(request, username):
def handle_remote_webfinger(query):
''' webfingerin' other servers '''
''' webfingerin' other servers, username query should be user@domain '''
user = None
domain = query.split('@')[1]
try:
domain = query.split('@')[2]
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' % \
url = 'https://%s/.well-known/webfinger?resource=acct:@%s' % \
(domain, query)
try:
response = requests.get(url)
@ -55,7 +62,7 @@ def handle_remote_webfinger(query):
user = get_or_create_remote_user(link['href'])
except KeyError:
return None
return [user]
return user
def handle_follow(user, to_follow):
@ -211,7 +218,36 @@ def handle_status(user, form):
''' generic handler for statuses '''
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:
create_notification(
status.reply_parent.user,

View file

@ -22,6 +22,10 @@
favorited your
<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' %}
<a href="{{ notification.related_status.remote_id}}">replied</a>
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 goodreads_import
from bookwyrm.tasks import app
from bookwyrm.utils import regex
def get_user_from_username(username):
@ -168,7 +169,7 @@ def search(request):
return JsonResponse([r.__dict__ for r in book_results], safe=False)
# 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)
# do a local user search

View file

@ -1,8 +1,9 @@
''' responds to various requests to /.well-know '''
from datetime import datetime
from dateutil.relativedelta import relativedelta
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.http import HttpResponseNotFound
from django.http import JsonResponse
from bookwyrm import models
@ -16,13 +17,16 @@ def webfinger(request):
resource = request.GET.get('resource')
if not resource and not resource.startswith('acct:'):
return HttpResponseBadRequest()
ap_id = resource.replace('acct:', '')
user = models.User.objects.filter(username=ap_id).first()
if not user:
return HttpResponseNotFound()
username = resource.replace('acct:@', '')
try:
user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
return HttpResponseNotFound('No account found')
return JsonResponse({
'subject': 'acct:%s' % (user.username),
'subject': 'acct:@%s' % (user.username),
'links': [
{
'rel': 'self',