Merge pull request #388 from mouse-reeve/fix-create-sttatus

Handle incoming statuses correctly
This commit is contained in:
Mouse Reeve 2020-12-13 16:22:20 -08:00 committed by GitHub
commit a7ee461b97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 416 additions and 138 deletions

View file

@ -4,8 +4,6 @@ from json import JSONEncoder
from django.apps import apps
from django.db import transaction
from django.db.models.fields.files import ImageFileDescriptor
from django.db.models.fields.related_descriptors import ManyToManyDescriptor
from bookwyrm.connectors import ConnectorException, get_data
from bookwyrm.tasks import app
@ -77,55 +75,30 @@ class ActivityObject:
)
# check for an existing instance, if we're not updating a known obj
if not instance:
instance = model.find_existing(self.serialize()) or model()
instance = instance or model.find_existing(self.serialize()) or model()
many_to_many_fields = {}
image_fields = {}
for field in model._meta.get_fields():
# check if it's an activitypub field
if not hasattr(field, 'field_to_activity'):
continue
# call the formatter associated with the model field class
value = field.field_from_activity(
getattr(self, field.get_activitypub_field())
)
if value is None or value is MISSING:
continue
for field in instance.simple_fields:
field.set_field_from_activity(instance, self)
model_field = getattr(model, field.name)
if isinstance(model_field, ManyToManyDescriptor):
# status mentions book/users for example, stash this for later
many_to_many_fields[field.name] = value
elif isinstance(model_field, ImageFileDescriptor):
# image fields need custom handling
image_fields[field.name] = value
else:
# just a good old fashioned model.field = value
setattr(instance, field.name, value)
# if this isn't here, it messes up saving users. who even knows.
for (model_key, value) in image_fields.items():
getattr(instance, model_key).save(*value, save=save)
# image fields have to be set after other fields because they can save
# too early and jank up users
for field in instance.image_fields:
field.set_field_from_activity(instance, self, save=save)
if not save:
# we can't set many to many and reverse fields on an unsaved object
return instance
# we can't set many to many and reverse fields on an unsaved object
instance.save()
# add many to many fields, which have to be set post-save
for (model_key, values) in many_to_many_fields.items():
for field in instance.many_to_many_fields:
# mention books/users, for example
getattr(instance, model_key).set(values)
if not save or not hasattr(model, 'deserialize_reverse_fields'):
return instance
field.set_field_from_activity(instance, self)
# reversed relationships in the models
for (model_field_name, activity_field_name) in \
model.deserialize_reverse_fields:
instance.deserialize_reverse_fields:
# attachments on Status, for example
values = getattr(self, activity_field_name)
if values is None or values is MISSING:

View file

@ -185,12 +185,13 @@ def handle_follow_reject(activity):
def handle_create(activity):
''' someone did something, good on them '''
# deduplicate incoming activities
status_id = activity['object']['id']
activity = activity['object']
status_id = activity['id']
if models.Status.objects.filter(remote_id=status_id).count():
return
serializer = activitypub.activity_objects[activity['type']]
status = serializer(**activity)
activity = serializer(**activity)
try:
model = models.activity_models[activity.type]
except KeyError:
@ -198,13 +199,25 @@ def handle_create(activity):
return
if activity.type == 'Note':
# keep notes if they are replies to existing statuses
reply = models.Status.objects.filter(
remote_id=activity.inReplyTo
).first()
if not reply:
return
activity.to_model(model)
if not reply:
discard = True
# keep notes if they mention local users
tags = [l['href'] for l in activity.tag if l['type'] == 'Mention']
for tag in tags:
if models.User.objects.filter(
remote_id=tag, local=True).exists():
# we found a mention of a known use boost
discard = False
break
if discard:
return
status = activity.to_model(model)
# create a notification if this is a reply
if status.reply_parent and status.reply_parent.user.local:
status_builder.create_notification(

View file

@ -25,8 +25,3 @@ from .site import SiteSettings, SiteInvite, PasswordReset
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
activity_models = {c[1].activity_serializer.__name__: c[1] \
for c in cls_members if hasattr(c[1], 'activity_serializer')}
def to_activity(activity_json):
''' link up models and activities '''
activity_type = activity_json.get('type')
return activity_models[activity_type].to_activity(activity_json)

View file

@ -14,16 +14,9 @@ from django.dispatch import receiver
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN, PAGE_LENGTH
from .fields import RemoteIdField
from .fields import ImageField, ManyToManyField, RemoteIdField
PrivacyLevels = models.TextChoices('Privacy', [
'public',
'unlisted',
'followers',
'direct'
])
class BookWyrmModel(models.Model):
''' shared fields '''
created_date = models.DateTimeField(auto_now_add=True)
@ -68,6 +61,33 @@ class ActivitypubMixin:
activity_serializer = lambda: {}
reverse_unfurl = False
def __init__(self, *args, **kwargs):
''' collect some info on model fields '''
self.image_fields = []
self.many_to_many_fields = []
self.simple_fields = [] # "simple"
for field in self._meta.get_fields():
if not hasattr(field, 'field_to_activity'):
continue
if isinstance(field, ImageField):
self.image_fields.append(field)
elif isinstance(field, ManyToManyField):
self.many_to_many_fields.append(field)
else:
self.simple_fields.append(field)
self.activity_fields = self.image_fields + \
self.many_to_many_fields + self.simple_fields
self.deserialize_reverse_fields = self.deserialize_reverse_fields \
if hasattr(self, 'deserialize_reverse_fields') else []
self.serialize_reverse_fields = self.serialize_reverse_fields \
if hasattr(self, 'serialize_reverse_fields') else []
super().__init__(*args, **kwargs)
@classmethod
def find_existing_by_remote_id(cls, remote_id):
''' look up a remote id in the db '''
@ -115,19 +135,8 @@ class ActivitypubMixin:
def to_activity(self):
''' convert from a model to an activity '''
activity = {}
for field in self._meta.get_fields():
if not hasattr(field, 'field_to_activity'):
continue
value = field.field_to_activity(getattr(self, field.name))
if value is None:
continue
key = field.get_activitypub_field()
if key in activity and isinstance(activity[key], list):
# handles tags on status, which accumulate across fields
activity[key] += value
else:
activity[key] = value
for field in self.activity_fields:
field.set_activity_from_field(activity, self)
if hasattr(self, 'serialize_reverse_fields'):
# for example, editions of a work

View file

@ -1,4 +1,5 @@
''' activitypub-aware django model fields '''
from dataclasses import MISSING
import re
from uuid import uuid4
@ -38,6 +39,30 @@ class ActivitypubFieldMixin:
self.activitypub_field = activitypub_field
super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data):
''' helper function for assinging a value to the field '''
value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING:
return
setattr(instance, self.name, formatted)
def set_activity_from_field(self, activity, instance):
''' update the json object '''
value = getattr(instance, self.name)
formatted = self.field_to_activity(value)
if formatted is None:
return
key = self.get_activitypub_field()
if isinstance(activity.get(key), list):
activity[key] += formatted
else:
activity[key] = formatted
def field_to_activity(self, value):
''' formatter to convert a model value into activitypub '''
if hasattr(self, 'activitypub_wrapper'):
@ -123,6 +148,52 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
return value.split('@')[0]
PrivacyLevels = models.TextChoices('Privacy', [
'public',
'unlisted',
'followers',
'direct'
])
class PrivacyField(ActivitypubFieldMixin, models.CharField):
''' this maps to two differente activitypub fields '''
public = 'https://www.w3.org/ns/activitystreams#Public'
def __init__(self, *args, **kwargs):
super().__init__(
*args, max_length=255,
choices=PrivacyLevels.choices, default='public')
def set_field_from_activity(self, instance, data):
to = data.to
cc = data.cc
if to == [self.public]:
setattr(instance, self.name, 'public')
elif cc == []:
setattr(instance, self.name, 'direct')
elif self.public in cc:
setattr(instance, self.name, 'unlisted')
else:
setattr(instance, self.name, 'followers')
def set_activity_from_field(self, activity, instance):
mentions = [u.remote_id for u in instance.mention_users.all()]
# this is a link to the followers list
followers = instance.user.__class__._meta.get_field('followers')\
.field_to_activity(instance.user.followers)
if instance.privacy == 'public':
activity['to'] = [self.public]
activity['cc'] = [followers] + mentions
elif instance.privacy == 'unlisted':
activity['to'] = [followers]
activity['cc'] = [self.public] + mentions
elif instance.privacy == 'followers':
activity['to'] = [followers]
activity['cc'] = mentions
if instance.privacy == 'direct':
activity['to'] = mentions
activity['cc'] = []
class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
''' activitypub-aware foreign key field '''
def field_to_activity(self, value):
@ -145,6 +216,14 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
self.link_only = link_only
super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data):
''' helper function for assinging a value to the field '''
value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING:
return
getattr(instance, self.name).set(formatted)
def field_to_activity(self, value):
if self.link_only:
return '%s/%s' % (value.instance.remote_id, self.name)
@ -210,9 +289,20 @@ def image_serializer(value):
class ImageField(ActivitypubFieldMixin, models.ImageField):
''' activitypub-aware image field '''
# pylint: disable=arguments-differ
def set_field_from_activity(self, instance, data, save=True):
''' helper function for assinging a value to the field '''
value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING:
return
getattr(instance, self.name).save(*formatted, save=save)
def field_to_activity(self, value):
return image_serializer(value)
def field_from_activity(self, value):
image_slug = value
# when it's an inline image (User avatar/icon, Book cover), it's a json

View file

@ -8,7 +8,7 @@ from django.utils import timezone
from bookwyrm import books_manager
from bookwyrm.models import ReadThrough, User, Book
from .base_model import PrivacyLevels
from .fields import PrivacyLevels
# Mapping goodreads -> bookwyrm shelf titles.

View file

@ -4,7 +4,7 @@ from django.db import models
from bookwyrm import activitypub
from .base_model import BookWyrmModel
from .base_model import OrderedCollectionMixin, PrivacyLevels
from .base_model import OrderedCollectionMixin
from . import fields
@ -18,7 +18,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
privacy = fields.CharField(
max_length=255,
default='public',
choices=PrivacyLevels.choices
choices=fields.PrivacyLevels.choices
)
books = models.ManyToManyField(
'Edition',

View file

@ -6,7 +6,7 @@ from model_utils.managers import InheritanceManager
from bookwyrm import activitypub
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from .base_model import BookWyrmModel, PrivacyLevels
from .base_model import BookWyrmModel
from . import fields
from .fields import image_serializer
@ -18,13 +18,9 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
mention_users = fields.TagField('User', related_name='mention_user')
mention_books = fields.TagField('Edition', related_name='mention_book')
local = models.BooleanField(default=True)
privacy = models.CharField(
max_length=255,
default='public',
choices=PrivacyLevels.choices
)
privacy = fields.PrivacyField(max_length=255)
sensitive = fields.BooleanField(default=False)
# the created date can't be this, because of receiving federated posts
# created date is different than publish date because of federated posts
published_date = fields.DateTimeField(
default=timezone.now, activitypub_field='published')
deleted = models.BooleanField(default=False)
@ -48,12 +44,13 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
serialize_reverse_fields = [('attachments', 'attachment')]
deserialize_reverse_fields = [('attachments', 'attachment')]
#----- replies collection activitypub ----#
@classmethod
def replies(cls, status):
''' load all replies to a status. idk if there's a better way
to write this so it's just a property '''
return cls.objects.filter(reply_parent=status).select_subclasses()
return cls.objects.filter(
reply_parent=status
).select_subclasses().order_by('published_date')
@property
def status_type(self):
@ -68,7 +65,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
**kwargs
)
def to_activity(self, pure=False):
def to_activity(self, pure=False):# pylint: disable=arguments-differ
''' return tombstone if the status is deleted '''
if self.deleted:
return activitypub.Tombstone(
@ -80,25 +77,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
activity = ActivitypubMixin.to_activity(self)
activity['replies'] = self.to_replies()
# privacy controls
public = 'https://www.w3.org/ns/activitystreams#Public'
mentions = [u.remote_id for u in self.mention_users.all()]
# this is a link to the followers list:
followers = self.user.__class__._meta.get_field('followers')\
.field_to_activity(self.user.followers)
if self.privacy == 'public':
activity['to'] = [public]
activity['cc'] = [followers] + mentions
elif self.privacy == 'unlisted':
activity['to'] = [followers]
activity['cc'] = [public] + mentions
elif self.privacy == 'followers':
activity['to'] = [followers]
activity['cc'] = mentions
if self.privacy == 'direct':
activity['to'] = mentions
activity['cc'] = []
# "pure" serialization for non-bookwyrm instances
if pure:
activity['content'] = self.pure_content
@ -190,6 +168,7 @@ class Review(Status):
def pure_name(self):
''' clarify review names for mastodon serialization '''
if self.rating:
#pylint: disable=bad-string-format-type
return 'Review of "%s" (%d stars): %s' % (
self.book.title,
self.rating,

View file

@ -0,0 +1,37 @@
{% extends 'layout.html' %}
{% block content %}
<div class="block">
<h1 class="title">Direct Messages</h1>
{% if not activities %}
<p>You have no messages right now.</p>
{% endif %}
{% for activity in activities %}
<div class="block">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %}
<nav class="pagination" role="navigation" aria-label="pagination">
{% if prev %}
<p class="pagination-previous">
<a href="{{ prev }}">
<span class="icon icon-arrow-left"></span>
Previous
</a>
</p>
{% endif %}
{% if next %}
<p class="pagination-next">
<a href="{{ next }}">
Next
<span class="icon icon-arrow-right"></span>
</a>
</p>
{% endif %}
</nav>
</div>
{% endblock %}

View file

@ -68,6 +68,9 @@
{% include 'snippets/username.html' with user=request.user %}
</p></div>
<div class="navbar-dropdown">
<a href="/direct-messages" class="navbar-item">
Direct messages
</a>
<a href="/user/{{request.user.localname}}" class="navbar-item">
Profile
</a>

View file

@ -13,7 +13,7 @@
<div class="column is-narrow">
<div class="field">
{% include 'snippets/privacy_select.html' %}
{% include 'snippets/privacy_select.html' with current=activity.privacy %}
</div>
<div class="field">
<button class="button is-primary" type="submit">

View file

@ -95,34 +95,67 @@ class BaseActivity(TestCase):
self.assertEqual(result.remote_id, 'https://example.com/user/mouse')
self.assertEqual(result.name, 'MOUSE?? MOUSE!!')
def test_to_model(self):
''' the big boy of this module. it feels janky to test this with actual
models rather than a test model, but I don't know how to make a test
model so here we are. '''
def test_to_model_invalid_model(self):
''' catch mismatch between activity type and model type '''
instance = ActivityObject(id='a', type='b')
with self.assertRaises(ActivitySerializerError):
instance.to_model(models.User)
# test setting simple fields
def test_to_model_simple_fields(self):
''' test setting simple fields '''
self.assertEqual(self.user.name, '')
update_data = activitypub.Person(**self.user.to_activity())
update_data.name = 'New Name'
update_data.to_model(models.User, self.user)
activity = activitypub.Person(
id=self.user.remote_id,
name='New Name',
preferredUsername='mouse',
inbox='http://www.com/',
outbox='http://www.com/',
followers='',
summary='',
publicKey=None,
endpoints={},
)
activity.to_model(models.User, self.user)
self.assertEqual(self.user.name, 'New Name')
def test_to_model_foreign_key(self):
''' test setting one to one/foreign key '''
update_data = activitypub.Person(**self.user.to_activity())
update_data.publicKey['publicKeyPem'] = 'hi im secure'
update_data.to_model(models.User, self.user)
activity = activitypub.Person(
id=self.user.remote_id,
name='New Name',
preferredUsername='mouse',
inbox='http://www.com/',
outbox='http://www.com/',
followers='',
summary='',
publicKey=self.user.key_pair.to_activity(),
endpoints={},
)
activity.publicKey['publicKeyPem'] = 'hi im secure'
activity.to_model(models.User, self.user)
self.assertEqual(self.user.key_pair.public_key, 'hi im secure')
@responses.activate
def test_to_model_image(self):
''' update an image field '''
update_data = activitypub.Person(**self.user.to_activity())
update_data.icon = {'url': 'http://www.example.com/image.jpg'}
activity = activitypub.Person(
id=self.user.remote_id,
name='New Name',
preferredUsername='mouse',
inbox='http://www.com/',
outbox='http://www.com/',
followers='',
summary='',
publicKey=None,
endpoints={},
icon={'url': 'http://www.example.com/image.jpg'}
)
responses.add(
responses.GET,
'http://www.example.com/image.jpg',
@ -133,7 +166,7 @@ class BaseActivity(TestCase):
with self.assertRaises(ValueError):
self.user.avatar.file #pylint: disable=pointless-statement
update_data.to_model(models.User, self.user)
activity.to_model(models.User, self.user)
self.assertIsNotNone(self.user.avatar.name)
self.assertIsNotNone(self.user.avatar.file)
@ -145,19 +178,26 @@ class BaseActivity(TestCase):
)
book = models.Edition.objects.create(
title='Test Edition', remote_id='http://book.com/book')
update_data = activitypub.Note(**status.to_activity())
update_data.tag = [
{
'type': 'Mention',
'name': 'gerald',
'href': 'http://example.com/a/b'
},
{
'type': 'Edition',
'name': 'gerald j. books',
'href': 'http://book.com/book'
},
]
update_data = activitypub.Note(
id=status.remote_id,
content=status.content,
attributedTo=self.user.remote_id,
published='hi',
to=[],
cc=[],
tag=[
{
'type': 'Mention',
'name': 'gerald',
'href': 'http://example.com/a/b'
},
{
'type': 'Edition',
'name': 'gerald j. books',
'href': 'http://book.com/book'
},
]
)
update_data.to_model(models.Status, instance=status)
self.assertEqual(status.mention_users.first(), self.user)
self.assertEqual(status.mention_books.first(), book)
@ -171,12 +211,19 @@ class BaseActivity(TestCase):
content='test status',
user=self.user,
)
update_data = activitypub.Note(**status.to_activity())
update_data.attachment = [{
'url': 'http://www.example.com/image.jpg',
'name': 'alt text',
'type': 'Image',
}]
update_data = activitypub.Note(
id=status.remote_id,
content=status.content,
attributedTo=self.user.remote_id,
published='hi',
to=[],
cc=[],
attachment=[{
'url': 'http://www.example.com/image.jpg',
'name': 'alt text',
'type': 'Image',
}],
)
responses.add(
responses.GET,

View file

@ -1,9 +1,11 @@
''' testing models '''
from io import BytesIO
from collections import namedtuple
from dataclasses import dataclass
import json
import pathlib
import re
from typing import List
from unittest.mock import patch
from PIL import Image
@ -15,7 +17,9 @@ from django.db import models
from django.test import TestCase
from django.utils import timezone
from bookwyrm.models import fields, User
from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm.models import fields, User, Status
from bookwyrm.models.base_model import ActivitypubMixin, BookWyrmModel
class ActivitypubFields(TestCase):
''' overwrites standard model feilds to work with activitypub '''
@ -90,6 +94,97 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.field_to_activity('test@example.com'), 'test')
def test_privacy_field_defaults(self):
''' post privacy field's many default values '''
instance = fields.PrivacyField()
self.assertEqual(instance.max_length, 255)
self.assertEqual(
[c[0] for c in instance.choices],
['public', 'unlisted', 'followers', 'direct'])
self.assertEqual(instance.default, 'public')
self.assertEqual(
instance.public, 'https://www.w3.org/ns/activitystreams#Public')
def test_privacy_field_set_field_from_activity(self):
''' translate between to/cc fields and privacy '''
@dataclass(init=False)
class TestActivity(ActivityObject):
''' real simple mock '''
to: List[str]
cc: List[str]
id: str = 'http://hi.com'
type: str = 'Test'
class TestPrivacyModel(ActivitypubMixin, BookWyrmModel):
''' real simple mock model because BookWyrmModel is abstract '''
privacy_field = fields.PrivacyField()
mention_users = fields.TagField(User)
user = fields.ForeignKey(User, on_delete=models.CASCADE)
public = 'https://www.w3.org/ns/activitystreams#Public'
data = TestActivity(
to=[public],
cc=['bleh'],
)
model_instance = TestPrivacyModel(privacy_field='direct')
self.assertEqual(model_instance.privacy_field, 'direct')
instance = fields.PrivacyField()
instance.name = 'privacy_field'
instance.set_field_from_activity(model_instance, data)
self.assertEqual(model_instance.privacy_field, 'public')
data.to = ['bleh']
data.cc = []
instance.set_field_from_activity(model_instance, data)
self.assertEqual(model_instance.privacy_field, 'direct')
data.to = ['bleh']
data.cc = [public, 'waah']
instance.set_field_from_activity(model_instance, data)
self.assertEqual(model_instance.privacy_field, 'unlisted')
def test_privacy_field_set_activity_from_field(self):
''' translate between to/cc fields and privacy '''
user = User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword', local=True)
public = 'https://www.w3.org/ns/activitystreams#Public'
followers = '%s/followers' % user.remote_id
instance = fields.PrivacyField()
instance.name = 'privacy_field'
model_instance = Status.objects.create(user=user, content='hi')
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [public])
self.assertEqual(activity['cc'], [followers])
model_instance = Status.objects.create(user=user, privacy='unlisted')
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [followers])
self.assertEqual(activity['cc'], [public])
model_instance = Status.objects.create(user=user, privacy='followers')
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [followers])
self.assertEqual(activity['cc'], [])
model_instance = Status.objects.create(
user=user,
privacy='direct',
)
model_instance.mention_users.set([user])
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [user.remote_id])
self.assertEqual(activity['cc'], [])
def test_foreign_key(self):
''' should be able to format a related model '''
instance = fields.ForeignKey('User', on_delete=models.CASCADE)
@ -98,6 +193,7 @@ class ActivitypubFields(TestCase):
# returns the remote_id field of the related object
self.assertEqual(instance.field_to_activity(item), 'https://e.b/c')
@responses.activate
def test_foreign_key_from_activity_str(self):
''' create a new object from a foreign key '''

View file

@ -53,7 +53,8 @@ urlpatterns = [
path('', views.home),
re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab),
re_path(r'^notifications/?', views.notifications_page),
re_path(r'^notifications/?$', views.notifications_page),
re_path(r'^direct-messages/?$', views.direct_messages_page),
re_path(r'^import/?$', views.import_page),
re_path(r'^import-status/(\d+)/?$', views.import_status),
re_path(r'^user-edit/?$', views.edit_profile_page),

View file

@ -113,11 +113,36 @@ def get_suggested_books(user, max_books=5):
return suggested_books
@login_required
@require_GET
def direct_messages_page(request, page=1):
''' like a feed but for dms only '''
activities = get_activity_feed(request.user, 'direct')
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
prev_page = next_page = None
if activity_page.has_next():
next_page = '/direct-message/?page=%d#feed' % \
activity_page.next_page_number()
if activity_page.has_previous():
prev_page = '/direct-messages/?page=%d#feed' % \
activity_page.previous_page_number()
data = {
'title': 'Direct Messages',
'user': request.user,
'activities': activity_page.object_list,
'next': next_page,
'prev': prev_page,
}
return TemplateResponse(request, 'direct_messages.html', data)
def get_activity_feed(user, filter_level, model=models.Status):
''' get a filtered queryset of statuses '''
# status updates for your follow network
if user.is_anonymous:
user = None
if user:
following = models.User.objects.filter(
Q(followers=user) | Q(id=user.id)
@ -135,6 +160,16 @@ def get_activity_feed(user, filter_level, model=models.Status):
'-published_date'
)
if filter_level == 'direct':
return activities.filter(
Q(user=user) | Q(mention_users=user),
privacy='direct'
)
# never show DMs in the regular feed
activities = activities.filter(~Q(privacy='direct'))
if hasattr(activities, 'select_subclasses'):
activities = activities.select_subclasses()