mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-14 20:18:02 +00:00
Fix Bluesky integration caching and API session management
- Fix stale cache issue: assignment pages now show updated data immediately - Implement persistent session management to reduce createSession API calls - Add robust error handling for cache operations when Memcached unavailable - Eliminate code duplication in get_profile method with _make_profile_request - Add Bluesky credentials configuration to config_local.py.example Resolves caching problems and API rate limiting issues in development and production.
This commit is contained in:
@@ -44,3 +44,7 @@ import os
|
||||
|
||||
# Example: Override logs directory for production
|
||||
# LOGS_DIR = "/var/log/ivatar"
|
||||
|
||||
# Bluesky integration credentials
|
||||
# BLUESKY_IDENTIFIER = "your-bluesky-handle"
|
||||
# BLUESKY_APP_PASSWORD = "your-app-password"
|
||||
|
||||
@@ -398,9 +398,28 @@ class ConfirmedEmail(BaseAccountModel):
|
||||
)
|
||||
|
||||
cache_key = f"views.decorators.cache.cache_page.{quote(str(cache_url))}"
|
||||
if cache.has_key(cache_key):
|
||||
cache.delete(cache_key)
|
||||
logger.debug("Successfully cleaned up cached page: %s" % cache_key)
|
||||
try:
|
||||
if cache.has_key(cache_key):
|
||||
cache.delete(cache_key)
|
||||
logger.debug("Successfully cleaned up cached page: %s" % cache_key)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to clean up cached page %s: %s" % (cache_key, exc)
|
||||
)
|
||||
|
||||
# Invalidate Bluesky avatar URL cache if bluesky_handle changed
|
||||
if hasattr(self, "bluesky_handle") and self.bluesky_handle:
|
||||
try:
|
||||
cache.delete(self.bluesky_handle)
|
||||
logger.debug(
|
||||
"Successfully cleaned up Bluesky avatar cache for handle: %s"
|
||||
% self.bluesky_handle
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to clean up Bluesky avatar cache for handle %s: %s"
|
||||
% (self.bluesky_handle, exc)
|
||||
)
|
||||
|
||||
return super().save(force_insert, force_update, using, update_fields)
|
||||
|
||||
@@ -570,6 +589,36 @@ class ConfirmedOpenId(BaseAccountModel):
|
||||
openid_variations(lowercase_url)[3].encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
# Invalidate page caches and Bluesky avatar cache
|
||||
if self.pk:
|
||||
# Invalidate assign_photo_openid page cache
|
||||
cache_url = reverse_lazy(
|
||||
"assign_photo_openid", kwargs={"openid_id": int(self.pk)}
|
||||
)
|
||||
cache_key = f"views.decorators.cache.cache_page.{quote(str(cache_url))}"
|
||||
try:
|
||||
if cache.has_key(cache_key):
|
||||
cache.delete(cache_key)
|
||||
logger.debug("Successfully cleaned up cached page: %s" % cache_key)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to clean up cached page %s: %s" % (cache_key, exc)
|
||||
)
|
||||
|
||||
# Invalidate Bluesky avatar URL cache if bluesky_handle exists
|
||||
if hasattr(self, "bluesky_handle") and self.bluesky_handle:
|
||||
try:
|
||||
cache.delete(self.bluesky_handle)
|
||||
logger.debug(
|
||||
"Successfully cleaned up Bluesky avatar cache for handle: %s"
|
||||
% self.bluesky_handle
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to clean up Bluesky avatar cache for handle %s: %s"
|
||||
% (self.bluesky_handle, exc)
|
||||
)
|
||||
|
||||
return super().save(force_insert, force_update, using, update_fields)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -23,7 +23,7 @@ django.setup()
|
||||
# pylint: disable=wrong-import-position
|
||||
from ivatar import settings
|
||||
from ivatar.ivataraccount.models import ConfirmedOpenId, ConfirmedEmail
|
||||
from ivatar.utils import random_string
|
||||
from ivatar.utils import random_string, Bluesky
|
||||
|
||||
from libravatar import libravatar_url
|
||||
|
||||
@@ -63,6 +63,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
)
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
|
||||
|
||||
# Clear any existing Bluesky session to ensure clean test state
|
||||
Bluesky.clear_shared_session()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Clean up after tests
|
||||
"""
|
||||
# Clear Bluesky session to avoid affecting other tests
|
||||
Bluesky.clear_shared_session()
|
||||
|
||||
def create_confirmed_openid(self):
|
||||
"""
|
||||
Create a confirmed openid
|
||||
|
||||
@@ -36,13 +36,15 @@ def urlopen(url, timeout=URL_TIMEOUT):
|
||||
|
||||
class Bluesky:
|
||||
"""
|
||||
Handle Bluesky client access
|
||||
Handle Bluesky client access with persistent session management
|
||||
"""
|
||||
|
||||
identifier = ""
|
||||
app_password = ""
|
||||
service = "https://bsky.social"
|
||||
session = None
|
||||
_shared_session = None # Class-level shared session
|
||||
_session_expires_at = None # Track session expiration
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -54,10 +56,29 @@ class Bluesky:
|
||||
self.app_password = app_password
|
||||
self.service = service
|
||||
|
||||
def _is_session_valid(self) -> bool:
|
||||
"""
|
||||
Check if the current session is still valid
|
||||
"""
|
||||
if not self._shared_session or not self._session_expires_at:
|
||||
return False
|
||||
|
||||
import time
|
||||
|
||||
# Add 5 minute buffer before actual expiration
|
||||
return time.time() < (self._session_expires_at - 300)
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Login to Bluesky
|
||||
Login to Bluesky with session persistence
|
||||
"""
|
||||
# Use shared session if available and valid
|
||||
if self._is_session_valid():
|
||||
self.session = self._shared_session
|
||||
logger.debug("Reusing existing Bluesky session")
|
||||
return
|
||||
|
||||
logger.debug("Creating new Bluesky session")
|
||||
auth_response = requests.post(
|
||||
f"{self.service}/xrpc/com.atproto.server.createSession",
|
||||
json={"identifier": self.identifier, "password": self.app_password},
|
||||
@@ -65,6 +86,29 @@ class Bluesky:
|
||||
auth_response.raise_for_status()
|
||||
self.session = auth_response.json()
|
||||
|
||||
# Store session data for reuse
|
||||
self._shared_session = self.session
|
||||
import time
|
||||
|
||||
# Sessions typically expire in 24 hours, but we'll refresh every 12 hours
|
||||
self._session_expires_at = time.time() + (12 * 60 * 60)
|
||||
|
||||
logger.debug(
|
||||
"Created new Bluesky session, expires at: %s",
|
||||
time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S", time.localtime(self._session_expires_at)
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def clear_shared_session(cls):
|
||||
"""
|
||||
Clear the shared session (useful for testing)
|
||||
"""
|
||||
cls._shared_session = None
|
||||
cls._session_expires_at = None
|
||||
logger.debug("Cleared shared Bluesky session")
|
||||
|
||||
def normalize_handle(self, handle: str) -> str:
|
||||
"""
|
||||
Return the normalized handle for given handle
|
||||
@@ -79,11 +123,10 @@ class Bluesky:
|
||||
handle = handle[:-1]
|
||||
return handle
|
||||
|
||||
def get_profile(self, handle: str) -> str:
|
||||
if not self.session:
|
||||
self.login()
|
||||
profile_response = None
|
||||
|
||||
def _make_profile_request(self, handle: str):
|
||||
"""
|
||||
Make a profile request to Bluesky API with automatic retry on session expiration
|
||||
"""
|
||||
try:
|
||||
profile_response = requests.get(
|
||||
f"{self.service}/xrpc/app.bsky.actor.getProfile",
|
||||
@@ -91,11 +134,32 @@ class Bluesky:
|
||||
params={"actor": handle},
|
||||
)
|
||||
profile_response.raise_for_status()
|
||||
return profile_response.json()
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
if exc.response.status_code == 401:
|
||||
# Session expired, try to login again
|
||||
logger.warning("Bluesky session expired, re-authenticating")
|
||||
self.clear_shared_session()
|
||||
self.login()
|
||||
# Retry the request
|
||||
profile_response = requests.get(
|
||||
f"{self.service}/xrpc/app.bsky.actor.getProfile",
|
||||
headers={"Authorization": f'Bearer {self.session["accessJwt"]}'},
|
||||
params={"actor": handle},
|
||||
)
|
||||
profile_response.raise_for_status()
|
||||
return profile_response.json()
|
||||
else:
|
||||
logger.warning(f"Bluesky profile fetch failed with HTTP error: {exc}")
|
||||
return None
|
||||
except Exception as exc:
|
||||
logger.warning(f"Bluesky profile fetch failed with HTTP error: {exc}")
|
||||
logger.warning(f"Bluesky profile fetch failed with error: {exc}")
|
||||
return None
|
||||
|
||||
return profile_response.json()
|
||||
def get_profile(self, handle: str) -> str:
|
||||
if not self.session or not self._is_session_valid():
|
||||
self.login()
|
||||
return self._make_profile_request(handle)
|
||||
|
||||
def get_avatar(self, handle: str):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user