@ -0,0 +1,310 @@
import requests
import json
from django . conf import settings
from django . contrib . auth import get_user_model
from django . core . mail import send_mail
from django . core . exceptions import ObjectDoesNotExist
from django . db import models
from django . db . models . signals import post_save , post_delete
from django . dispatch import receiver
from django . template import loader
from django . urls import NoReverseMatch , get_resolver , reverse
from django . utils . translation import ugettext_lazy as _
from djangoldp . fields import LDPUrlField
from djangoldp . models import Model
from djangoldp . views import LDPViewSet
from djangoldp . activities . services import ActivityQueueService , ActivityPubService , activity_sending_finished
from djangoldp_notification . middlewares import MODEL_MODIFICATION_USER_FIELD
from djangoldp_notification . permissions import InboxPermissions , SubscriptionsPermissions
from djangoldp_notification . views import LDPNotificationsViewSet
from djangoldp_webpushnotification . views import _send_push
import logging
logger = logging . getLogger ( ' djangoldp ' )
class Notification ( Model ) :
user = models . ForeignKey ( settings . AUTH_USER_MODEL , related_name = ' inbox ' , on_delete = models . deletion . CASCADE )
author = LDPUrlField ( )
object = LDPUrlField ( )
type = models . CharField ( max_length = 255 )
summary = models . TextField ( )
date = models . DateTimeField ( auto_now_add = True )
unread = models . BooleanField ( default = True )
class Meta ( Model . Meta ) :
owner_field = ' user '
ordering = [ ' -date ' ]
permission_classes = [ InboxPermissions ]
anonymous_perms = [ ' add ' ]
authenticated_perms = [ ' inherit ' ]
owner_perms = [ ' view ' , ' change ' , ' control ' ]
view_set = LDPNotificationsViewSet
# NOTE: this would be our ideal cache behaviour
# the functionality for optimising it was removed because of an issue with extensibility
# https://git.startinblox.com/djangoldp-packages/djangoldp-notification/merge_requests/42#note_58559
''' def clear_djangoldp_cache(self, cache, cache_entry):
# should only clear the users/x/inbox
lookup_arg = LDPViewSet.get_lookup_arg(model=get_user_model())
url = reverse( ' {} - {} -list ' .format(self.user.__class__.__name__.lower(), self.__class__.__name__.lower()),
args=[getattr(self.user, lookup_arg)])
url = ' {} {} ' .format(settings.SITE_URL, url)
cache.invalidate(cache_entry, url)
# invalidate the global /notifications/ container also
url = ' {} {} ' .format(settings.SITE_URL, reverse( ' {} -list ' .format(self.__class__.__name__.lower())))
cache.invalidate(cache_entry, url) '''
def __str__ ( self ) :
return ' {} ' . format ( self . type )
def save ( self , * args , * * kwargs ) :
# I cannot send a notification to myself
if self . author . startswith ( settings . SITE_URL ) :
try :
# author is a WebID.. convert to local representation
author = Model . resolve ( self . author . replace ( settings . SITE_URL , ' ' ) ) [ 1 ]
except NoReverseMatch :
author = None
if author == self . user :
return
super ( Notification , self ) . save ( * args , * * kwargs )
class NotificationSetting ( Model ) :
user = models . OneToOneField ( settings . AUTH_USER_MODEL , on_delete = models . CASCADE , related_name = " settings " )
receiveMail = models . BooleanField ( default = True )
class Meta :
auto_author = ' user '
owner_field = ' user '
anonymous_perms = [ ]
authenticated_perms = [ ]
owner_perms = [ ' view ' , ' change ' ]
container_path = ' settings/ '
serializer_fields = [ ' @id ' , ' receiveMail ' ]
rdf_type = ' sib:usersettings '
def __str__ ( self ) :
return ' {} ( {} ) ' . format ( self . user . get_full_name ( ) , self . user . urlid )
class Subscription ( Model ) :
object = models . URLField ( )
inbox = models . URLField ( )
field = models . CharField ( max_length = 255 , blank = True , null = True ,
help_text = ' if set to a field name on the object model, the field will be passed instead of the object instance ' )
def __str__ ( self ) :
return ' {} ' . format ( self . object )
class Meta ( Model . Meta ) :
anonymous_perms = [ ]
authenticated_perms = [ " add " , " view " , " delete " ]
permission_classes = [ SubscriptionsPermissions ]
@receiver ( post_save , sender = Subscription , dispatch_uid = " nested_subscriber_check " )
def create_nested_subscribers ( sender , instance , created , * * kwargs ) :
# save subscriptions for one-to-many nested fields
if created and not instance . is_backlink and instance . object . startswith ( settings . SITE_URL ) :
try :
# object is a WebID.. convert to local representation
local = Model . resolve ( instance . object . replace ( settings . SITE_URL , ' ' ) ) [ 0 ]
nested_fields = Model . get_meta ( local , ' nested_fields ' , [ ] )
# Don't create nested subscriptions for user model (Notification loop issue)
if local . _meta . model_name == get_user_model ( ) . _meta . model_name :
return
for nested_field in nested_fields :
try :
field = local . _meta . get_field ( nested_field )
nested_container = field . related_model
nested_container_url = Model . absolute_url ( nested_container )
if field . one_to_many :
# get the nested view set
nested_url = str ( instance . object ) + ' 1/ ' + nested_field + ' / '
view , args , kwargs = get_resolver ( ) . resolve ( nested_url . replace ( settings . SITE_URL , ' ' ) )
# get the reverse name for the field
field_name = view . initkwargs [ ' nested_related_name ' ]
if field_name is not None and field_name != ' ' :
# check that this nested-field subscription doesn't already exist
existing_subscriptions = Subscription . objects . filter ( object = nested_container_url , inbox = instance . inbox ,
field = field_name )
# save a Subscription on this container
if not existing_subscriptions . exists ( ) :
Subscription . objects . create ( object = nested_container_url , inbox = instance . inbox , is_backlink = True ,
field = field_name )
except :
pass
except :
pass
# --- SUBSCRIPTION SYSTEM ---
@receiver ( post_save , dispatch_uid = " callback_notif " )
@receiver ( post_delete , dispatch_uid = " delete_callback_notif " )
def send_notification ( sender , instance , * * kwargs ) :
if ( type ( instance ) . __name__ != " ScheduledActivity " and type ( instance ) . __name__ != " LogEntry " and type ( instance ) . __name__ != " Activity " ) :
if sender != Notification :
# don't send notifications for foreign resources
if hasattr ( instance , ' urlid ' ) and Model . is_external ( instance . urlid ) :
return
recipients = [ ]
try :
url_container = settings . BASE_URL + Model . container_id ( instance )
url_resource = settings . BASE_URL + Model . resource_id ( instance )
except NoReverseMatch :
return
# dispatch a notification for every Subscription on this resource
for subscription in Subscription . objects . filter ( models . Q ( object = url_resource ) | models . Q ( object = url_container ) ) :
if subscription . inbox not in recipients and ( not subscription . is_backlink or not kwargs . get ( " created " ) ) :
# I may have configured to send the subscription to a foreign key
if subscription . field is not None and len ( subscription . field ) > 1 :
try :
if kwargs . get ( " created " ) :
continue
instance = getattr ( instance , subscription . field , instance )
# don't send notifications for foreign resources
if hasattr ( instance , ' urlid ' ) and Model . is_external ( instance . urlid ) :
continue
url_resource = settings . BASE_URL + Model . resource_id ( instance )
except NoReverseMatch :
continue
except ObjectDoesNotExist :
continue
send_request ( subscription . inbox , url_resource , instance , kwargs . get ( " created " ) )
recipients . append ( subscription . inbox )
@receiver ( activity_sending_finished , sender = ActivityQueueService )
def _handle_prosody_response ( sender , response , saved_activity , * * kwargs ) :
''' callback function for handling a response from Prosody on a notification '''
# if text is defined in the response body then it's an error
if saved_activity is not None :
response_body = saved_activity . response_to_json ( )
if ' condition ' in response_body :
logger . error ( " [DjangoLDP-Notification.models._handle_prosody_response] error in Prosody response " +
str ( response_body ) )
def send_request ( target , object_iri , instance , created ) :
author = getattr ( getattr ( instance , MODEL_MODIFICATION_USER_FIELD , None ) , " urlid " , str ( _ ( " Auteur inconnu " ) ) )
if ( created is not None ) :
request_type = " creation " if created else " update "
else :
request_type = " deletion "
# local inbox
if target . startswith ( settings . SITE_URL ) :
user = Model . resolve_parent ( target . replace ( settings . SITE_URL , ' ' ) )
Notification . objects . create ( user = user , object = object_iri , type = request_type , author = author )
# external inbox
else :
json = {
" @context " : settings . LDP_RDF_CONTEXT ,
" object " : object_iri ,
" author " : author ,
" type " : request_type
}
ActivityQueueService . send_activity ( target , json )
@receiver ( post_save , sender = Notification )
def send_email_on_notification ( sender , instance , created , * * kwargs ) :
if created and instance . summary and getattr ( settings , ' JABBER_DEFAULT_HOST ' , False ) and instance . user . email :
# get author name, and store in who
try :
# local author
if instance . author . startswith ( settings . SITE_URL ) :
who = str ( Model . resolve_id ( instance . author . replace ( settings . SITE_URL , ' ' ) ) . get_full_name ( ) )
# external author
else :
who = requests . get ( instance . author ) . json ( ) [ ' name ' ]
except :
who = " Quelqu ' un "
# get identifier for resource triggering notification, and store in where
try :
if instance . object . startswith ( settings . SITE_URL ) :
if hasattr ( Model . resolve_id ( instance . object . replace ( settings . SITE_URL , ' ' ) ) , ' get_full_name ' ) :
where = Model . resolve_id ( instance . object . replace ( settings . SITE_URL , ' ' ) ) . get_full_name ( )
else :
where = str ( Model . resolve_id ( instance . object . replace ( settings . SITE_URL , ' ' ) ) . name )
else :
where = requests . get ( instance . object ) . json ( ) [ ' name ' ]
except :
where = " le chat "
if who == where :
where = " t ' a envoyé un message privé "
else :
where = " t ' a mentionné sur " + where
on = ( getattr ( settings , ' INSTANCE_DEFAULT_CLIENT ' , False ) or settings . JABBER_DEFAULT_HOST )
html_message = loader . render_to_string (
' email.html ' ,
{
' on ' : on ,
' instance ' : instance ,
' author ' : who ,
' object ' : where
}
)
if instance . user . settings . receiveMail :
send_mail (
' Notification sur ' + on ,
instance . summary ,
( getattr ( settings , ' EMAIL_HOST_USER ' , False ) or " noreply@ " + settings . JABBER_DEFAULT_HOST ) ,
[ instance . user . email ] ,
fail_silently = True ,
html_message = html_message
)
@receiver ( post_save , sender = Notification )
def send_webpush_on_notification ( sender , instance , created , * * kwargs ) :
if instance . summary :
if instance . user : # if the person exists and accepts notifications
try :
# local author
if instance . author . startswith ( settings . SITE_URL ) :
who = str ( Model . resolve_id ( instance . author . replace ( settings . SITE_URL , ' ' ) ) . get_full_name ( ) )
# external author
else :
who = requests . get ( instance . author ) . json ( ) [ ' name ' ]
except :
who = " Quelqu ' un "
payload = {
" head " : " Notification from " + who ,
" body " : instance . summary ,
" id " : instance . user . id
} # make the payload that we wanna send?
_send_push ( payload )
@receiver ( post_save , sender = settings . AUTH_USER_MODEL )
def create_user_settings ( sender , instance , created , * * kwargs ) :
try :
if created and instance . urlid . startswith ( settings . SITE_URL ) :
NotificationSetting . objects . create ( user = instance )
except :
pass