Switching to key-based APNS cert.

This commit is contained in:
Samuel Clay 2025-04-25 18:18:22 -07:00
parent b32201a570
commit 9cd40a71e8
4 changed files with 70 additions and 32 deletions

View file

@ -77,6 +77,10 @@ lint:
docker exec -it newsblur_web isort --profile black .
docker exec -it newsblur_web black --line-length 110 .
docker exec -it newsblur_web flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=venv
deps:
docker exec -t newsblur_web pip install -U uv
docker exec -t newsblur_web uv pip install -r requirements.txt
jekyll_build:
cd blog && JEKYLL_ENV=production bundle exec jekyll build

View file

@ -7,9 +7,8 @@ import urllib.parse
import mongoengine as mongo
import redis
from apns2.client import APNsClient
from apns2.errors import BadDeviceToken, DeviceTokenNotForTopic, Unregistered
from apns2.payload import Payload
from pyapns_client import APNSClient, IOSPayloadAlert, IOSPayload, IOSNotification
from pyapns_client import APNSDeviceException, APNSServerException, APNSProgrammingException, UnregisteredException
from bs4 import BeautifulSoup
from django.conf import settings
from django.contrib.auth.models import User
@ -306,7 +305,7 @@ class MUserFeedNotification(mongo.Document):
# 4. Save the key file to secrets/certificates/ios/apns_key.p8
# 5. Note your Team ID and Key ID
# 6. Deploy: aps -l work -t apns,repo,celery
# Legacy certificate method (kept for reference):
# 0. Upgrade to latest openssl: brew install openssl
# 1. Create certificate signing request in Keychain Access
@ -321,22 +320,24 @@ class MUserFeedNotification(mongo.Document):
# 7. cat aps.pem aps_key.noenc.pem > aps.p12.pem
# 8. Verify: openssl s_client -connect gateway.push.apple.com:2195 -cert aps.p12.pem
# 9. Deploy: aps -l work -t apns,repo,celery
# Using token-based authentication (modern method)
# Using token-based authentication (modern method with pyapns-client)
key_file_path = "/srv/newsblur/config/certificates/apns_key.p8"
apns = APNsClient(
team_id=settings.APNS_TEAM_ID,
auth_key_id=settings.APNS_KEY_ID,
auth_key_path=key_file_path,
use_sandbox=tokens.use_sandbox
)
notification_title_only = is_true(user.profile.preference_value("notification_title_only"))
title, subtitle, body = self.title_and_body(story, usersub, notification_title_only)
image_url = None
if len(story["image_urls"]):
image_url = story["image_urls"][0]
# print image_url
# Create APNS client
apns = APNSClient(
mode=APNSClient.MODE_DEV if tokens.use_sandbox else APNSClient.MODE_PROD,
root_cert_path=None,
auth_key_path=key_file_path,
auth_key_id=settings.APNS_KEY_ID,
team_id=settings.APNS_TEAM_ID
)
confirmed_ios_tokens = []
for token in tokens.ios_tokens:
@ -345,27 +346,44 @@ class MUserFeedNotification(mongo.Document):
"~BMStory notification by iOS: ~FY~SB%s~SN~BM~FY/~SB%s"
% (story["story_title"][:50], usersub.feed.feed_title[:50]),
)
payload = Payload(
alert={"title": title, "subtitle": subtitle, "body": body},
# Create payload using helper classes
alert = IOSPayloadAlert(title=title, subtitle=subtitle, body=body)
custom_data = {
"story_hash": story["story_hash"],
"story_feed_id": story["story_feed_id"],
}
if image_url:
custom_data["image_url"] = image_url
payload = IOSPayload(
alert=alert,
custom=custom_data,
category="STORY_CATEGORY",
mutable_content=True,
custom={
"story_hash": story["story_hash"],
"story_feed_id": story["story_feed_id"],
"image_url": image_url,
},
mutable_content=image_url is not None
)
notification = IOSNotification(payload=payload, topic="com.newsblur.NewsBlur")
try:
apns.send_notification(token, payload, topic="com.newsblur.NewsBlur")
except (BadDeviceToken, Unregistered, DeviceTokenNotForTopic):
logging.user(user, "~BMiOS token expired: ~FR~SB%s" % (token[:50]))
else:
apns.push(notification=notification, device_token=token)
confirmed_ios_tokens.append(token)
if settings.DEBUG:
logging.user(
user,
"~BMiOS token good: ~FB~SB%s / %s" % (token[:50], len(confirmed_ios_tokens)),
)
except UnregisteredException as e:
logging.user(user, "~BMiOS token unregistered: ~FR~SB%s (since %s)" % (token[:50], e.timestamp_datetime))
except APNSDeviceException as e:
logging.user(user, "~BMiOS token invalid: ~FR~SB%s" % (token[:50]))
except APNSServerException as e:
logging.user(user, "~BMiOS notification server error: ~FR~SB%s - %s" % (token[:50], str(e)))
except APNSProgrammingException as e:
logging.user(user, "~BMiOS notification programming error: ~FR~SB%s - %s" % (token[:50], str(e)))
except Exception as e:
logging.user(user, "~BMiOS notification error: ~FR~SB%s - %s" % (token[:50], str(e)))
finally:
apns.close()
if len(confirmed_ios_tokens) < len(tokens.ios_tokens):
tokens.ios_tokens = confirmed_ios_tokens

View file

@ -1,5 +1,4 @@
amqp==2.6.1
apns2==0.7.2
appdirs==1.4.4
asgiref==3.3.4
attrs==21.1.0
@ -46,13 +45,8 @@ Flask-BasicAuth==0.2.0
future==0.18.2
gunicorn==21.2.0
gevent==22.10.2
h2==2.6.2
hiredis==1.1.0
hpack==3.0.0
httplib2==0.18.1
httpx==0.27.2
hyper==0.7.0
hyperframe==3.2.0
idna==2.10
image==1.5.33
iniconfig==1.1.1
@ -81,9 +75,9 @@ pluggy==0.13.1
psutil==5.7.3
psycopg2==2.9.2
py==1.10.0
pyapns-client==2.0.6
pyasn1==0.4.8
pycparser==2.20
PyJWT==1.7.1
pymongo>=3,<4
PyMySQL==0.10.1
pynliner==0.8.0

View file

@ -61,6 +61,17 @@ class AppDirectoriesFinder(PipelineAppDirectoriesFinder):
"*LICENSE*",
"*README*",
]
def find_files(self, storage, path=None, all=False):
"""
Override to properly handle wildcard patterns like 'underscore-*.js'
"""
path = path or ''
for pattern in self.find_pattern_matches(path):
for path in storage.listdir(pattern[0])[1]:
if self.is_ignored(path, pattern[0]):
continue
yield path, storage
class FileSystemFinder(PipelineFileSystemFinder):
@ -115,3 +126,14 @@ class FileSystemFinder(PipelineFileSystemFinder):
# 'Gemfile*',
"node_modules",
]
def find_files(self, storage, path=None, all=False):
"""
Override to properly handle wildcard patterns like 'underscore-*.js'
"""
path = path or ''
for pattern in self.find_pattern_matches(path):
for path in storage.listdir(pattern[0])[1]:
if self.is_ignored(path, pattern[0]):
continue
yield path, storage