mirror of
https://github.com/tanrax/org-social-relay
synced 2026-07-25 02:34:58 +02:00
Expose external accounts as virtual Org Social feeds that any client
can follow with a plain #+FOLLOW: line:
- /bridge/activitypub/@{user}@{instance}/ bridges a Mastodon or any
ActivityPub account (WebFinger, actor and paginated outbox; only
public top-level notes, with CW, hashtags, language and attachments)
- /bridge/rss/?url={feed} bridges any RSS/Atom feed (entry title as
*** sub-heading, converted body and link to the original article)
Registration is implicit on first GET. Bridged data is stored in the
existing Profile/Post tables so /profile/, /search/ and the rest of
the API work on bridged feeds. Active bridges are refreshed every 15
minutes; bridges unrequested for 90 days are cleaned up.
Remote HTML is converted to Org text, escaping headline-like lines so
external content cannot inject posts. Fetches enforce the Webmention
SSRF protections, a 10s timeout and a 5MB size cap. Bridged posts
never queue Webmentions nor publish notifications.
111 lines
3.2 KiB
Python
111 lines
3.2 KiB
Python
"""
|
|
Periodic maintenance of bridged accounts.
|
|
|
|
Only bridges requested recently are refreshed; dormant ones are
|
|
reactivated by the inline refresh on their next GET, and bridges nobody
|
|
has requested for a long time are deleted together with their profiles.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import timedelta
|
|
|
|
from huey import crontab
|
|
from huey.contrib.djhuey import periodic_task
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Bridges accessed within this window are refreshed periodically
|
|
BRIDGE_ACTIVE_DAYS = 7
|
|
# Bridges not accessed for this long are deleted
|
|
BRIDGE_STALE_DAYS = 90
|
|
|
|
|
|
def _refresh_bridges_impl():
|
|
"""
|
|
Refresh every bridge accessed within the active window.
|
|
|
|
Returns:
|
|
dict: Counters of refreshed / failed bridges
|
|
"""
|
|
from django.utils import timezone
|
|
|
|
from .fetching import BridgeError
|
|
from .models import BridgedActivityPubAccount, BridgedRssFeed
|
|
from .store import refresh_activitypub_bridge, refresh_rss_bridge
|
|
|
|
cutoff = timezone.now() - timedelta(days=BRIDGE_ACTIVE_DAYS)
|
|
counters = {"refreshed": 0, "failed": 0}
|
|
|
|
refresh_plan = [
|
|
(BridgedActivityPubAccount, refresh_activitypub_bridge),
|
|
(BridgedRssFeed, refresh_rss_bridge),
|
|
]
|
|
|
|
for model, refresh in refresh_plan:
|
|
for bridge in model.objects.filter(last_accessed_at__gte=cutoff):
|
|
try:
|
|
refresh(bridge)
|
|
counters["refreshed"] += 1
|
|
except BridgeError as e:
|
|
counters["failed"] += 1
|
|
logger.warning(f"Failed to refresh bridge {bridge}: {e}")
|
|
except Exception as e:
|
|
counters["failed"] += 1
|
|
logger.error(f"Unexpected error refreshing bridge {bridge}: {e}")
|
|
|
|
logger.info(
|
|
f"Bridge refresh completed. "
|
|
f"Refreshed: {counters['refreshed']}, Failed: {counters['failed']}"
|
|
)
|
|
return counters
|
|
|
|
|
|
@periodic_task(crontab(minute="*/15")) # Run every 15 minutes
|
|
def refresh_bridges():
|
|
"""Periodic task to keep active bridged accounts up to date."""
|
|
import django
|
|
|
|
django.setup()
|
|
|
|
return _refresh_bridges_impl()
|
|
|
|
|
|
def _cleanup_stale_bridges_impl():
|
|
"""
|
|
Delete bridges (and their profiles with all posts) that nobody has
|
|
requested in BRIDGE_STALE_DAYS days.
|
|
|
|
Returns:
|
|
int: Number of bridges deleted
|
|
"""
|
|
from django.utils import timezone
|
|
|
|
from app.feeds.models import Profile
|
|
|
|
cutoff = timezone.now() - timedelta(days=BRIDGE_STALE_DAYS)
|
|
|
|
# Deleting the Profile cascades to the bridge row and its posts
|
|
stale_profiles = Profile.objects.filter(
|
|
activitypub_bridge__last_accessed_at__lt=cutoff
|
|
) | Profile.objects.filter(rss_bridge__last_accessed_at__lt=cutoff)
|
|
|
|
deleted = 0
|
|
for profile in stale_profiles:
|
|
logger.info(f"Deleting stale bridge profile: {profile.feed}")
|
|
profile.delete()
|
|
deleted += 1
|
|
|
|
if deleted:
|
|
logger.info(f"Stale bridge cleanup completed. Deleted {deleted} bridges")
|
|
return deleted
|
|
|
|
|
|
@periodic_task(crontab(day="*/3", hour=3, minute=0)) # Every 3 days at 3 AM
|
|
def cleanup_stale_bridges():
|
|
"""Periodic task to delete bridges nobody requests anymore."""
|
|
import django
|
|
|
|
django.setup()
|
|
|
|
return _cleanup_stale_bridges_impl()
|