combo/combo/apps/pwa/signals.py

107 lines
3.3 KiB
Python

# combo - content management system
# Copyright (C) 2015-2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import hashlib
import json
import logging
import urllib.parse
import pywebpush
from django.conf import settings
from django.core.cache import cache
from django.db.models.signals import post_save
from django.dispatch import receiver
from py_vapid import Vapid
from combo.apps.notifications.models import Notification
from .models import PushSubscription, PwaSettings
logger = logging.getLogger(__name__)
def get_vapid_headers(private_key, subscription_info):
url = urllib.parse.urlparse(subscription_info['endpoint'])
aud = f'{url.scheme}://{url.netloc}'
key_bytes = private_key.encode('ascii')
cache_key = 'vapid-headers-' + hashlib.sha256(aud.encode() + key_bytes).hexdigest()
headers = cache.get(cache_key)
if headers:
return headers
pwa_vapid_private_key = Vapid.from_pem(key_bytes)
headers = pwa_vapid_private_key.sign(
{
'aud': aud,
'sub': 'mailto:%s' % settings.DEFAULT_FROM_EMAIL,
'exp': int(datetime.datetime.now().timestamp() + 3600 * 24), # expire after 24 hours
}
)
cache.set(cache_key, headers, 23 * 3600) # but keep it 23 hours
return headers
class DeadSubscription(Exception):
pass
def send_webpush(private_key, subscription_info, **kwargs):
message = json.dumps(kwargs)
headers = get_vapid_headers(private_key, subscription_info)
webpusher = pywebpush.WebPusher(subscription_info)
response = webpusher.send(
data=message,
headers=headers,
ttl=86400 * 30,
)
if response.status_code in (404, 410):
raise DeadSubscription
response.raise_for_status()
@receiver(post_save, sender=Notification)
def notification(sender, instance=None, created=False, **kwargs):
if not created:
return
pwa_settings = PwaSettings.singleton()
if not pwa_settings.push_notifications:
return
private_key = pwa_settings.push_notifications_infos['private_key']
for subscription in PushSubscription.objects.filter(user=instance.user):
try:
send_webpush(
private_key=private_key,
subscription_info=subscription.subscription_info,
summary=instance.summary,
body=instance.body,
url=instance.url,
)
logger.info('webpush: notification sent')
except DeadSubscription:
subscription.delete()
logger.info('webpush: deleting dead subscription')
except Exception:
logger.exception('webpush: request failed')