Compare commits

...

31 Commits

Author SHA1 Message Date
Guillem Hernandez Sola
baa055a36e Added some rss fixes 2026-04-18 11:18:55 +02:00
Guillem Hernandez Sola
6565c62a7a Meteocat 2026-04-18 11:04:34 +02:00
Guillem Hernandez Sola
08cb7e18e3 New login hardening 2026-04-18 10:58:25 +02:00
Guillem Hernandez Sola
bdeec32a25 Added some login fixes 2026-04-18 10:50:47 +02:00
Guillem Hernandez Sola
4a83d526f9 Added blacksky meteocat 2026-04-18 10:23:54 +02:00
Guillem Hernandez Sola
48710862ab Added blacksky meteocat 2026-04-18 10:21:36 +02:00
16c04ff7d3 Added 429 control removed eurosky on jenkinsfile 2026-04-18 09:03:17 +02:00
e3f1932775 Added 429 control removed eurosky on jenkinsfile 2026-04-18 09:01:50 +02:00
05b69da411 Added 429 control 2026-04-18 08:54:15 +02:00
Guillem Hernandez Sola
bd79ddd40c migrated mesqueunbot 2026-04-17 18:01:51 +02:00
Guillem Hernandez Sola
d48646fba8 migrated catalunya112 2026-04-17 17:34:53 +02:00
Guillem Hernandez Sola
e7ac3c9b68 migrated esport3 2026-04-17 17:30:28 +02:00
Guillem Hernandez Sola
ffb0105a49 migrated apm 2026-04-17 17:25:14 +02:00
Guillem Hernandez Sola
036b5483b3 beteve migrated 2026-04-17 17:12:04 +02:00
Guillem Hernandez Sola
3b0d3b6412 3cat migrated 2026-04-17 17:03:33 +02:00
Guillem Hernandez Sola
22b903f35b Added optimot 2026-04-17 16:44:32 +02:00
9fde78b048 Migrated la xarxa més 2026-04-17 11:39:05 +02:00
Guillem Hernandez Sola
2d7a86c2cf ePrss 2026-04-17 09:05:33 +02:00
Guillem Hernandez Sola
3b3edaf409 Added rac1 to eurosky 2026-04-16 21:01:15 +02:00
Guillem Hernandez Sola
e4e1cf634e Added all 2026-04-16 19:55:07 +02:00
1f14a2cec4 revert dc451475eb
revert cleanWS
2026-04-15 08:42:54 +00:00
dc451475eb cleanWS 2026-04-15 08:38:41 +02:00
4a416d8468 Updared RSS 2026-04-14 19:33:04 +00:00
4d04ba245a revert 300d45192c
revert Updated RSS
2026-04-14 19:31:10 +00:00
e3e443c0c0 Updated 3CatTw 2026-04-14 19:30:01 +00:00
3fc74cb008 3CatTw Jenkinsfile 2026-04-14 19:29:09 +00:00
300d45192c Updated RSS 2026-04-14 19:04:14 +00:00
f11b0a1677 Updated jenkins file 2026-04-14 19:02:21 +00:00
Guillem Hernandez Sola
344b2d56a3 remove versioRAC1 2026-04-14 18:32:04 +02:00
Guillem Hernandez Sola
17219e006f Added refactor on RSS 2026-04-14 18:32:04 +02:00
Guillem Hernandez Sola
ed85113d27 Added refactor on RSS 2026-04-14 18:32:04 +02:00
42 changed files with 1399 additions and 974 deletions

View File

@@ -2,7 +2,7 @@ pipeline {
agent any
options {
timeout(time: 15, unit: 'MINUTES')
timeout(time: 20, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
@@ -12,10 +12,15 @@ pipeline {
cron('H/30 * * * *')
}
environment {
VENV_DIR = 'venv'
PIP_CACHE_DIR = "${WORKSPACE}/.pip-cache"
PYTHONUNBUFFERED = '1'
}
stages {
stage('Checkout Code') {
steps {
// Pulls the code from the repository where this Jenkinsfile lives
checkout scm
}
}
@@ -23,34 +28,41 @@ pipeline {
stage('Setup Python & Install Dependencies') {
steps {
sh '''
set -e # Exit immediately if a command exits with a non-zero status
# Create a virtual environment named 'venv'
python3 -m venv venv
# Activate the virtual environment and install dependencies
. venv/bin/activate && \
pip install --upgrade pip && \
pip install -U atproto tweety-ns playwright httpx arrow python-dotenv moviepy
# Check if moviepy is installed
pip list | grep moviepy || { echo 'MoviePy installation failed!'; exit 1; }
# Check if FFmpeg is installed
ffmpeg -version || { echo 'FFmpeg is not installed!'; exit 1; }
set -euxo pipefail
# Verify that moviepy can be imported
python3 -c "import moviepy" || { echo 'MoviePy import failed!'; exit 1; }
python3 -m venv "${VENV_DIR}"
# Install the local browser binaries for this environment
playwright install chromium
# Always use venv python explicitly
"${VENV_DIR}/bin/python" -m pip install --upgrade pip wheel setuptools
"${VENV_DIR}/bin/pip" install --cache-dir "${PIP_CACHE_DIR}" -U \
atproto \
tweety-ns \
playwright \
httpx \
arrow \
python-dotenv \
moviepy \
fastfeedparser \
beautifulsoup4 \
charset-normalizer \
Pillow
# Verify required imports
"${VENV_DIR}/bin/python" -c "import fastfeedparser; print('fastfeedparser OK')"
"${VENV_DIR}/bin/python" -c "import moviepy; print('moviepy OK')"
# Check FFmpeg
ffmpeg -version
# Install Playwright browser binaries in this workspace environment
"${VENV_DIR}/bin/python" -m playwright install chromium
'''
}
}
stage('Run Script') {
steps {
// Securely injects Jenkins credentials as environment variables
withCredentials([
string(credentialsId: 'TWITTER_USERNAME', variable: 'TWITTER_USERNAME'),
string(credentialsId: 'TWITTER_PASSWORD', variable: 'TWITTER_PASSWORD'),
@@ -60,18 +72,26 @@ pipeline {
string(credentialsId: 'BSKY_3CAT_APP_PASSWORD', variable: 'BSKY_3CAT_APP_PASSWORD')
]) {
sh '''
# Activate the virtual environment and run the script
. venv/bin/activate && \
python3 twitter2bsky_daemon.py \
--twitter-username "$TWITTER_USERNAME" \
--twitter-password "$TWITTER_PASSWORD" \
--twitter-email "$TWITTER_3CAT_EMAIL" \
--twitter-handle "$TWITTER_3CAT_HANDLE" \
--bsky-handle "$BSKY_3CAT_HANDLE" \
--bsky-password "$BSKY_3CAT_APP_PASSWORD"
set -euxo pipefail
"${VENV_DIR}/bin/python" twitter2bsky_daemon.py \
--twitter-username "$TWITTER_USERNAME" \
--twitter-password "$TWITTER_PASSWORD" \
--twitter-email "$TWITTER_3CAT_EMAIL" \
--twitter-handle "$TWITTER_3CAT_HANDLE" \
--bsky-handle "$BSKY_3CAT_HANDLE" \
--bsky-password "$BSKY_3CAT_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}
}
}
}
post {
always {
// Optional: keep logs/artifacts if your script writes any
archiveArtifacts artifacts: '*.log, *.json', allowEmptyArchive: true
}
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_FCB_EMAIL" \
--twitter-handle "$TWITTER_FCBFUTBOLSALA_HANDLE" \
--bsky-handle "$BSKY_MQUB_HANDLE" \
--bsky-password "$BSKY_MQUB_PASSWORD"
--bsky-password "$BSKY_MQUB_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_FCB_EMAIL" \
--twitter-handle "$TWITTER_FCBHANDBOL_HANDLE" \
--bsky-handle "$BSKY_MQUB_HANDLE" \
--bsky-password "$BSKY_MQUB_PASSWORD"
--bsky-password "$BSKY_MQUB_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_FCB_EMAIL" \
--twitter-handle "$TWITTER_FCBHOQUEI_HANDLE" \
--bsky-handle "$BSKY_MQUB_HANDLE" \
--bsky-password "$BSKY_MQUB_PASSWORD"
--bsky-password "$BSKY_MQUB_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_FCB_EMAIL" \
--twitter-handle "$TWITTER_FCBMASIA_HANDLE" \
--bsky-handle "$BSKY_MQUB_HANDLE" \
--bsky-password "$BSKY_MQUB_PASSWORD"
--bsky-password "$BSKY_MQUB_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -1,77 +0,0 @@
pipeline {
agent any
options {
timeout(time: 15, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
}
triggers {
cron('H/30 * * * *')
}
stages {
stage('Checkout Code') {
steps {
// Pulls the code from the repository where this Jenkinsfile lives
checkout scm
}
}
stage('Setup Python & Install Dependencies') {
steps {
sh '''
set -e # Exit immediately if a command exits with a non-zero status
# Create a virtual environment named 'venv'
python3 -m venv venv
# Activate the virtual environment and install dependencies
. venv/bin/activate && \
pip install --upgrade pip && \
pip install -U atproto tweety-ns playwright httpx arrow python-dotenv moviepy
# Check if moviepy is installed
pip list | grep moviepy || { echo 'MoviePy installation failed!'; exit 1; }
# Check if FFmpeg is installed
ffmpeg -version || { echo 'FFmpeg is not installed!'; exit 1; }
# Verify that moviepy can be imported
python3 -c "import moviepy" || { echo 'MoviePy import failed!'; exit 1; }
# Install the local browser binaries for this environment
playwright install chromium
'''
}
}
stage('Run Script') {
steps {
// Securely injects Jenkins credentials as environment variables
withCredentials([
string(credentialsId: 'TWITTER_USERNAME', variable: 'TWITTER_USERNAME'),
string(credentialsId: 'TWITTER_PASSWORD', variable: 'TWITTER_PASSWORD'),
string(credentialsId: 'TWITTER_VERSIORAC1_EMAIL', variable: 'TWITTER_VERSIORAC1_EMAIL'),
string(credentialsId: 'TWITTER_VERSIORAC1_HANDLE', variable: 'TWITTER_VERSIORAC1_HANDLE'),
string(credentialsId: 'BSKY_VERSIORAC1_HANDLE', variable: 'BSKY_VERSIORAC1_HANDLE'),
string(credentialsId: 'BSKY_VERSIORAC1_APP_PASSWORD', variable: 'BSKY_VERSIORAC1_APP_PASSWORD')
]) {
sh '''
# Activate the virtual environment and run the script
. venv/bin/activate && \
python3 twitter2bsky_daemon.py \
--twitter-username "$TWITTER_USERNAME" \
--twitter-password "$TWITTER_PASSWORD" \
--twitter-email "$TWITTER_VERSIORAC1_EMAIL" \
--twitter-handle "$TWITTER_VERSIORAC1_HANDLE" \
--bsky-handle "$BSKY_VERSIORAC1_HANDLE" \
--bsky-password "$BSKY_VERSIORAC1_APP_PASSWORD"
'''
}
}
}
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_APM_EMAIL" \
--twitter-handle "$TWITTER_APM_HANDLE" \
--bsky-handle "$BSKY_APM_HANDLE" \
--bsky-password "$BSKY_APM_APP_PASSWORD"
--bsky-password "$BSKY_APM_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_BETEVE_EMAIL" \
--twitter-handle "$TWITTER_BETEVE_HANDLE" \
--bsky-handle "$BSKY_BETEVE_HANDLE" \
--bsky-password "$BSKY_BETEVE_APP_PASSWORD"
--bsky-password "$BSKY_BETEVE_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_CAT112_EMAIL" \
--twitter-handle "$TWITTER_BOMBERSCAT_HANDLE" \
--bsky-handle "$BSKY_CAT112_HANDLE" \
--bsky-password "$BSKY_CAT112_PASSWORD"
--bsky-password "$BSKY_CAT112_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -1,77 +0,0 @@
pipeline {
agent any
options {
timeout(time: 15, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
}
triggers {
cron('H/30 * * * *')
}
stages {
stage('Checkout Code') {
steps {
// Pulls the code from the repository where this Jenkinsfile lives
checkout scm
}
}
stage('Setup Python & Install Dependencies') {
steps {
sh '''
set -e # Exit immediately if a command exits with a non-zero status
# Create a virtual environment named 'venv'
python3 -m venv venv
# Activate the virtual environment and install dependencies
. venv/bin/activate && \
pip install --upgrade pip && \
pip install -U atproto tweety-ns playwright httpx arrow python-dotenv moviepy
# Check if moviepy is installed
pip list | grep moviepy || { echo 'MoviePy installation failed!'; exit 1; }
# Check if FFmpeg is installed
ffmpeg -version || { echo 'FFmpeg is not installed!'; exit 1; }
# Verify that moviepy can be imported
python3 -c "import moviepy" || { echo 'MoviePy import failed!'; exit 1; }
# Install the local browser binaries for this environment
playwright install chromium
'''
}
}
stage('Run Script') {
steps {
// Securely injects Jenkins credentials as environment variables
withCredentials([
string(credentialsId: 'TWITTER_USERNAME', variable: 'TWITTER_USERNAME'),
string(credentialsId: 'TWITTER_PASSWORD', variable: 'TWITTER_PASSWORD'),
string(credentialsId: 'TWITTER_BETEVE_EMAIL', variable: 'TWITTER_BETEVE_EMAIL'),
string(credentialsId: 'TWITTER_BTVBASICS_HANDLE', variable: 'TWITTER_BTVBASICS_HANDLE'),
string(credentialsId: 'BSKY_BETEVE_HANDLE', variable: 'BSKY_BETEVE_HANDLE'),
string(credentialsId: 'BSKY_BETEVE_APP_PASSWORD', variable: 'BSKY_BETEVE_APP_PASSWORD')
]) {
sh '''
# Activate the virtual environment and run the script
. venv/bin/activate && \
python3 twitter2bsky_daemon.py \
--twitter-username "$TWITTER_USERNAME" \
--twitter-password "$TWITTER_PASSWORD" \
--twitter-email "$TWITTER_BETEVE_EMAIL" \
--twitter-handle "$TWITTER_BTVBASICS_HANDLE" \
--bsky-handle "$BSKY_BETEVE_HANDLE" \
--bsky-password "$BSKY_BETEVE_APP_PASSWORD"
'''
}
}
}
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_BETEVE_EMAIL" \
--twitter-handle "$TWITTER_BTVESPORTS_HANDLE" \
--bsky-handle "$BSKY_BETEVE_HANDLE" \
--bsky-password "$BSKY_BETEVE_APP_PASSWORD"
--bsky-password "$BSKY_BETEVE_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_BETEVE_EMAIL" \
--twitter-handle "$TWITTER_BTVLLENGUA_HANDLE" \
--bsky-handle "$BSKY_BETEVE_HANDLE" \
--bsky-password "$BSKY_BETEVE_APP_PASSWORD"
--bsky-password "$BSKY_BETEVE_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_BETEVE_EMAIL" \
--twitter-handle "$TWITTER_BTVNOTICIES_HANDLE" \
--bsky-handle "$BSKY_BETEVE_HANDLE" \
--bsky-password "$BSKY_BETEVE_APP_PASSWORD"
--bsky-password "$BSKY_BETEVE_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -50,7 +50,8 @@ pipeline {
"https://cr-news-api-service.prd.crunchyrollsvc.com/v1/es-ES/rss" \
"$BSKY_CRUNCHYROLL_HANDLE" \
"$TWITTER_CRUNCHY_EMAIL" \
"$BSKY_CRUNCHYROLL_APP_PASSWORD"
"$BSKY_CRUNCHYROLL_APP_PASSWORD" \
--service "https://eurosky.social"
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_CRUNCHY_EMAIL" \
--twitter-handle "$TWITTER_CRUNCHYROLL_HANDLE" \
--bsky-handle "$BSKY_CRUNCHYROLL_HANDLE" \
--bsky-password "$BSKY_CRUNCHYROLL_APP_PASSWORD"
--bsky-password "$BSKY_CRUNCHYROLL_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -2,94 +2,148 @@ pipeline {
agent any
options {
timeout(time: 15, unit: 'MINUTES')
timeout(time: 30, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '10'))
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
triggers {
cron('H */6 * * *')
triggers {
cron('''
H 22,10,16 * * *
0 4 * * *
''')
}
environment {
VENV_DIR = 'venv'
MAX_PARALLEL_FEEDS = '4'
JITTER_MAX_SECONDS = '12'
PYTHONUNBUFFERED = '1'
PIP_CACHE_DIR = "${WORKSPACE}/.pip-cache"
}
stages {
stage('Checkout & Setup') {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Setup Python') {
steps {
sh '''
python3 -m venv venv
. venv/bin/activate
pip install atproto fastfeedparser beautifulsoup4 httpx arrow charset-normalizer Pillow
set -euxo pipefail
python3 -m venv "${VENV_DIR}"
. "${VENV_DIR}/bin/activate"
python -m pip install --upgrade pip wheel setuptools
pip install --cache-dir "${PIP_CACHE_DIR}" \
atproto fastfeedparser beautifulsoup4 httpx arrow charset-normalizer Pillow
python --version
pip --version
'''
}
}
stage('Process All RSS Feeds') {
stage('Process All RSS Feeds (Batched Parallel)') {
steps {
// 🔐 Fetch the single set of EP credentials ONCE for all 26 feeds
withCredentials([
string(credentialsId: 'BSKY_EP_HANDLE', variable: 'BSKY_EP_HANDLE'),
string(credentialsId: 'BSKY_EP_USERNAME', variable: 'BSKY_EP_USERNAME'),
string(credentialsId: 'BSKY_EP_APP_PASSWORD', variable: 'BSKY_EP_APP_PASSWORD')
]) {
script {
// 📍 The map only contains the hardcoded URLs
def feeds = [
// Original 19 Feeds
'badalona': 'https://www.elperiodico.cat/ca/rss/badalona/rss.xml',
'barcelona': 'https://www.elperiodico.cat/ca/rss/barcelona/rss.xml',
'ciencia': 'https://www.elperiodico.cat/ca/rss/ciencia/rss.xml',
'cornella': 'https://www.elperiodico.cat/ca/rss/cornella/rss.xml',
'economia': 'https://www.elperiodico.cat/ca/rss/economia/rss.xml',
'educacio': 'https://www.elperiodico.cat/ca/rss/educacio/rss.xml',
'esports': 'https://www.elperiodico.cat/ca/rss/esports/rss.xml',
'extra': 'https://www.elperiodico.cat/ca/rss/extra/rss.xml',
'gent': 'https://www.elperiodico.cat/ca/rss/gent/rss.xml',
'hospitalet': 'https://www.elperiodico.cat/ca/rss/hospitalet/rss.xml',
'internacional': 'https://www.elperiodico.cat/ca/rss/internacional/rss.xml',
'medi-ambient': 'https://www.elperiodico.cat/ca/rss/medi-ambient/rss.xml',
'motor': 'https://www.elperiodico.cat/ca/rss/motor/rss.xml',
'oci-i-cultura': 'https://www.elperiodico.cat/ca/rss/oci-i-cultura/rss.xml',
'opinio': 'https://www.elperiodico.cat/ca/rss/opinio/rss.xml',
'politica': 'https://www.elperiodico.cat/ca/rss/politica/rss.xml',
'portada': 'https://www.elperiodico.cat/ca/rss/portada/rss.xml',
'que-fer': 'https://www.elperiodico.cat/ca/rss/que-fer/rss.xml',
'sabadell': 'https://www.elperiodico.cat/ca/rss/sabadell/rss.xml',
// 7 New Feeds
'sanitat': 'https://www.elperiodico.cat/ca/rss/sanitat/rss.xml',
'santa-coloma': 'https://www.elperiodico.cat/ca/rss/santa-coloma/rss.xml',
'societat': 'https://www.elperiodico.cat/ca/rss/societat/rss.xml',
'tecnologia': 'https://www.elperiodico.cat/ca/rss/tecnologia/rss.xml',
'tele': 'https://www.elperiodico.cat/ca/rss/tele/rss.xml',
'temps': 'https://www.elperiodico.cat/ca/rss/temps/rss.xml',
'terrassa': 'https://www.elperiodico.cat/ca/rss/terrassa/rss.xml'
[name: 'badalona', url: 'https://www.elperiodico.cat/ca/rss/badalona/rss.xml'],
[name: 'barcelona', url: 'https://www.elperiodico.cat/ca/rss/barcelona/rss.xml'],
[name: 'ciencia', url: 'https://www.elperiodico.cat/ca/rss/ciencia/rss.xml'],
[name: 'cornella', url: 'https://www.elperiodico.cat/ca/rss/cornella/rss.xml'],
[name: 'economia', url: 'https://www.elperiodico.cat/ca/rss/economia/rss.xml'],
[name: 'educacio', url: 'https://www.elperiodico.cat/ca/rss/educacio/rss.xml'],
[name: 'esports', url: 'https://www.elperiodico.cat/ca/rss/esports/rss.xml'],
[name: 'extra', url: 'https://www.elperiodico.cat/ca/rss/extra/rss.xml'],
[name: 'gent', url: 'https://www.elperiodico.cat/ca/rss/gent/rss.xml'],
[name: 'hospitalet', url: 'https://www.elperiodico.cat/ca/rss/hospitalet/rss.xml'],
[name: 'internacional', url: 'https://www.elperiodico.cat/ca/rss/internacional/rss.xml'],
[name: 'medi-ambient', url: 'https://www.elperiodico.cat/ca/rss/medi-ambient/rss.xml'],
[name: 'motor', url: 'https://www.elperiodico.cat/ca/rss/motor/rss.xml'],
[name: 'oci-i-cultura', url: 'https://www.elperiodico.cat/ca/rss/oci-i-cultura/rss.xml'],
[name: 'opinio', url: 'https://www.elperiodico.cat/ca/rss/opinio/rss.xml'],
[name: 'politica', url: 'https://www.elperiodico.cat/ca/rss/politica/rss.xml'],
[name: 'portada', url: 'https://www.elperiodico.cat/ca/rss/portada/rss.xml'],
[name: 'que-fer', url: 'https://www.elperiodico.cat/ca/rss/que-fer/rss.xml'],
[name: 'sabadell', url: 'https://www.elperiodico.cat/ca/rss/sabadell/rss.xml'],
[name: 'sanitat', url: 'https://www.elperiodico.cat/ca/rss/sanitat/rss.xml'],
[name: 'santa-coloma', url: 'https://www.elperiodico.cat/ca/rss/santa-coloma/rss.xml'],
[name: 'societat', url: 'https://www.elperiodico.cat/ca/rss/societat/rss.xml'],
[name: 'tecnologia', url: 'https://www.elperiodico.cat/ca/rss/tecnologia/rss.xml'],
[name: 'tele', url: 'https://www.elperiodico.cat/ca/rss/tele/rss.xml'],
[name: 'temps', url: 'https://www.elperiodico.cat/ca/rss/temps/rss.xml'],
[name: 'terrassa', url: 'https://www.elperiodico.cat/ca/rss/terrassa/rss.xml']
]
def parallelTasks = [:]
int batchSize = env.MAX_PARALLEL_FEEDS as int
int jitterMax = env.JITTER_MAX_SECONDS as int
def batches = feeds.collate(batchSize)
feeds.each { feedName, feedUrl ->
parallelTasks[feedName] = {
stage(feedName.capitalize()) {
sh """
. venv/bin/activate
# The hardcoded URL and updated EP credentials are used here
python3 rss2bsky.py \\
"${feedUrl}" \\
"\$BSKY_EP_HANDLE" \\
"\$BSKY_EP_USERNAME" \\
"\$BSKY_EP_APP_PASSWORD"
"""
echo "Total feeds: ${feeds.size()}, batch size: ${batchSize}, batches: ${batches.size()}"
int batchNum = 0
for (def batch : batches) {
batchNum++
echo "Starting batch ${batchNum}/${batches.size()} with ${batch.size()} feeds"
def parallelTasks = [:]
batch.each { feed ->
def feedName = feed.name
def feedUrl = feed.url
parallelTasks[feedName] = {
stage("Feed: ${feedName}") {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
sh """
set -euxo pipefail
. "${VENV_DIR}/bin/activate"
JITTER=\$((RANDOM % ${jitterMax}))
echo "[${feedName}] sleeping \$JITTER s jitter"
sleep \$JITTER
python3 rss2bsky.py \\
"${feedUrl}" \\
"\$BSKY_EP_HANDLE" \\
"\$BSKY_EP_USERNAME" \\
"\$BSKY_EP_APP_PASSWORD" \
--service "https://eurosky.social"
"""
}
}
}
}
}
// Execute all 26 feeds simultaneously
parallel parallelTasks
parallel parallelTasks
echo "Finished batch ${batchNum}/${batches.size()}"
}
}
}
}
}
}
}
post {
always {
archiveArtifacts artifacts: '*.json, **/*.log', allowEmptyArchive: true
}
unstable {
echo 'Build unstable: one or more feeds failed, but pipeline completed.'
}
failure {
echo 'Build failed.'
}
success {
echo 'Build succeeded.'
}
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_RAC1_EMAIL" \
--twitter-handle "$TWITTER_ELMONARAC1_HANDLE" \
--bsky-handle "$BSKY_RAC1_HANDLE" \
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_CAT112_EMAIL" \
--twitter-handle "$TWITTER_EMERGENCIESCAT_HANDLE" \
--bsky-handle "$BSKY_CAT112_HANDLE" \
--bsky-password "$BSKY_CAT112_PASSWORD"
--bsky-password "$BSKY_CAT112_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -69,7 +69,8 @@ pipeline {
--twitter-email "$TWITTER_ESPORT3_EMAIL" \
--twitter-handle "$TWITTER_ESPORT3_HANDLE" \
--bsky-handle "$BSKY_ESPORT3_HANDLE" \
--bsky-password "$BSKY_ESPORT3_APP_PASSWORD"
--bsky-password "$BSKY_ESPORT3_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_RAC1_EMAIL" \
--twitter-handle "$TWITTER_ESPORTSRAC1_HANDLE" \
--bsky-handle "$BSKY_RAC1_HANDLE" \
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_LAXARXAMES_EMAIL" \
--twitter-handle "$TWITTER_ESPORTENXARXA_HANDLE" \
--bsky-handle "$BSKY_LAXARXAMES_HANDLE" \
--bsky-password "$BSKY_LAXARXAMES_APP_PASSWORD"
--bsky-password "$BSKY_LAXARXAMES_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_RAC1_EMAIL" \
--twitter-handle "$TWITTER_FCBAC1_HANDLE" \
--bsky-handle "$BSKY_RAC1_HANDLE" \
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_FCB_EMAIL" \
--twitter-handle "$TWITTER_FCB_HANDLE" \
--bsky-handle "$BSKY_MQUB_HANDLE" \
--bsky-password "$BSKY_MQUB_PASSWORD"
--bsky-password "$BSKY_MQUB_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_FCB_EMAIL" \
--twitter-handle "$TWITTER_FCBATLETIC_HANDLE" \
--bsky-handle "$BSKY_MQUB_HANDLE" \
--bsky-password "$BSKY_MQUB_PASSWORD"
--bsky-password "$BSKY_MQUB_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_FCB_EMAIL" \
--twitter-handle "$TWITTER_FCBBASKET_HANDLE" \
--bsky-handle "$BSKY_MQUB_HANDLE" \
--bsky-password "$BSKY_MQUB_PASSWORD"
--bsky-password "$BSKY_MQUB_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_FCB_EMAIL" \
--twitter-handle "$TWITTER_FCBFEMENI_HANDLE" \
--bsky-handle "$BSKY_MQUB_HANDLE" \
--bsky-password "$BSKY_MQUB_PASSWORD"
--bsky-password "$BSKY_MQUB_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_RAC1_EMAIL" \
--twitter-handle "$TWITTER_LACOMPETENCIARAC1_HANDLE" \
--bsky-handle "$BSKY_RAC1_HANDLE" \
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_LAXARXAMES_EMAIL" \
--twitter-handle "$TWITTER_LAXARXAMES_HANDLE" \
--bsky-handle "$BSKY_LAXARXAMES_HANDLE" \
--bsky-password "$BSKY_LAXARXAMES_APP_PASSWORD"
--bsky-password "$BSKY_LAXARXAMES_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_RAC1_EMAIL" \
--twitter-handle "$TWITTER_METEORAC1_HANDLE" \
--bsky-handle "$BSKY_RAC1_HANDLE" \
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_METEOCAT_EMAIL" \
--twitter-handle "$TWITTER_METEOCAT_HANDLE" \
--bsky-handle "$BSKY_METEOCAT_HANDLE" \
--bsky-password "$BSKY_METEOCAT_APP_PASSWORD"
--bsky-password "$BSKY_METEOCAT_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_CAT112_EMAIL" \
--twitter-handle "$TWITTER_MOSSOS_HANDLE" \
--bsky-handle "$BSKY_CAT112_HANDLE" \
--bsky-password "$BSKY_CAT112_PASSWORD"
--bsky-password "$BSKY_CAT112_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_RAC1_EMAIL" \
--twitter-handle "$TWITTER_NOHOSERAC1_HANDLE" \
--bsky-handle "$BSKY_RAC1_HANDLE" \
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_OPTIMOT_EMAIL" \
--twitter-handle "$TWITTER_OPTIMOT_HANDLE" \
--bsky-handle "$BSKY_OPTIMOT_HANDLE" \
--bsky-password "$BSKY_OPTIMOT_APP_PASSWORD"
--bsky-password "$BSKY_OPTIMOT_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_3CAT_EMAIL" \
--twitter-handle "$TWITTER_POLONIA3CAT_HANDLE" \
--bsky-handle "$BSKY_3CAT_HANDLE" \
--bsky-password "$BSKY_3CAT_APP_PASSWORD"
--bsky-password "$BSKY_3CAT_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -42,7 +42,8 @@ pipeline {
"https://www.rac1.cat/rss/home.xml" \\
"$BSKY_RAC1_HANDLE" \\
"$BSKY_RAC1_USERNAME" \\
"$BSKY_RAC1_APP_PASSWORD"
"$BSKY_RAC1_APP_PASSWORD" \
--service "https://eurosky.social"
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_CAT112_EMAIL" \
--twitter-handle "$TWITTER_SEMGENCAT_HANDLE" \
--bsky-handle "$BSKY_CAT112_HANDLE" \
--bsky-password "$BSKY_CAT112_PASSWORD"
--bsky-password "$BSKY_CAT112_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -71,7 +71,8 @@ pipeline {
--twitter-email "$TWITTER_CAT112_EMAIL" \
--twitter-handle "$TWITTER_TRANSIT_HANDLE" \
--bsky-handle "$BSKY_CAT112_HANDLE" \
--bsky-password "$BSKY_CAT112_PASSWORD"
--bsky-password "$BSKY_CAT112_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_RAC1_EMAIL" \
--twitter-handle "$TWITTER_VERSIORAC1_HANDLE" \
--bsky-handle "$BSKY_RAC1_HANDLE" \
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

View File

@@ -68,7 +68,8 @@ pipeline {
--twitter-email "$TWITTER_RAC1_EMAIL" \
--twitter-handle "$TWITTER_VIALLIURERAC1_HANDLE" \
--bsky-handle "$BSKY_RAC1_HANDLE" \
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
--bsky-base-url https://eurosky.social
'''
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,200 @@
import argparse
import logging
import os
import random
import sys
import time
import httpx
from atproto import Client
# --- Logging ---
LOG_PATH = "rss2bsky_test.log"
LOG_PATH = "bsky_login_test.log"
logging.basicConfig(
format="%(asctime)s %(message)s",
filename=LOG_PATH,
encoding="utf-8",
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LOG_PATH, encoding="utf-8"),
logging.StreamHandler(),
],
level=logging.INFO,
)
def main():
# --- Parse command-line arguments ---
parser = argparse.ArgumentParser(description="Post RSS to Bluesky.")
parser.add_argument("rss_feed", help="RSS feed URL")
parser.add_argument("bsky_handle", help="Bluesky handle")
parser.add_argument("bsky_username", help="Bluesky username")
parser.add_argument("bsky_app_password", help="Bluesky app password")
parser.add_argument("--service", default="https://bsky.social", help="Bluesky server URL (default: https://bsky.social)")
args = parser.parse_args()
bsky_username = args.bsky_username
bsky_password = args.bsky_app_password
service_url = args.service
EXIT_OK = 0
EXIT_BAD_CREDS = 2
EXIT_RATE_LIMIT = 3
EXIT_NETWORK = 4
EXIT_OTHER = 5
# --- Login ---
# SOLUCIÓ: Passem el base_url directament al constructor del Client
client = Client(base_url=service_url)
backoff = 60
while True:
def parse_wait_seconds_from_exception(exc, default_delay=15, max_delay=900):
"""
Parse common rate-limit headers from atproto exceptions:
- retry-after (seconds)
- x-ratelimit-after (seconds)
- ratelimit-reset (unix timestamp)
"""
try:
headers = getattr(exc, "headers", None) or {}
retry_after = headers.get("retry-after") or headers.get("Retry-After")
if retry_after:
return min(max(int(retry_after), 1), max_delay)
x_after = headers.get("x-ratelimit-after") or headers.get("X-RateLimit-After")
if x_after:
return min(max(int(x_after), 1), max_delay)
reset = headers.get("ratelimit-reset") or headers.get("RateLimit-Reset")
if reset:
wait_s = max(int(reset) - int(time.time()) + 1, 1)
return min(wait_s, max_delay)
except Exception:
pass
return default_delay
def classify_error(exc):
"""
Classify exception into:
- rate_limit
- bad_creds
- network
- other
"""
text = repr(exc).lower()
status_code = getattr(exc, "status_code", None)
if status_code == 429 or "429" in text or "too many requests" in text or "ratelimit" in text:
return "rate_limit"
if status_code in (401, 403) or "invalid identifier or password" in text or "authentication" in text:
return "bad_creds"
transient_signals = [
"timeout",
"connecterror",
"remoteprotocolerror",
"readtimeout",
"writetimeout",
"503",
"502",
"504",
"connection",
]
if any(sig in text for sig in transient_signals):
return "network"
return "other"
def preflight_health(service_url, timeout=8):
url = f"{service_url.rstrip('/')}/xrpc/_health"
try:
r = httpx.get(url, timeout=timeout)
logging.info(f"🩺 Health check {url} -> HTTP {r.status_code}")
return True
except Exception as e:
logging.warning(f"🩺 Health check failed: {e}")
return False
def build_client(service_url):
normalized = service_url.strip().rstrip("/")
try:
return Client(base_url=normalized)
except TypeError:
logging.warning("⚠️ Client(base_url=...) unsupported in this atproto version. Falling back.")
c = Client()
try:
logging.info(f"Attempting login to server: {service_url} with user: {bsky_username}")
client.login(bsky_username, bsky_password)
logging.info(f"Login successful for user: {bsky_username}")
break
if hasattr(c, "base_url"):
c.base_url = normalized
elif hasattr(c, "_base_url"):
c._base_url = normalized
except Exception as e:
logging.exception("Login exception")
time.sleep(backoff)
backoff = min(backoff + 60, 600)
logging.warning(f"⚠️ Could not apply custom base URL: {e}")
return c
def main():
parser = argparse.ArgumentParser(description="Bluesky login test only.")
parser.add_argument("--bsky-handle", required=True, help="Bluesky handle (e.g. user.example.social)")
parser.add_argument(
"--bsky-app-password",
default=None,
help="Bluesky app password (prefer env BSKY_APP_PASSWORD)",
)
parser.add_argument(
"--service",
default="https://bsky.social",
help="PDS base URL (default: https://bsky.social)",
)
parser.add_argument("--max-attempts", type=int, default=3, help="Retry attempts (default: 3)")
parser.add_argument("--base-delay", type=int, default=10, help="Base retry delay in seconds (default: 10)")
parser.add_argument("--jitter-max", type=float, default=2.0, help="Random jitter max seconds (default: 2.0)")
args = parser.parse_args()
handle = args.bsky_handle.strip()
service_url = args.service.strip().rstrip("/")
app_password = (args.bsky_app_password or os.getenv("BSKY_APP_PASSWORD", "")).strip()
if not app_password:
logging.error("❌ Missing app password. Use --bsky-app-password or env BSKY_APP_PASSWORD.")
print("LOGIN_FAILED_BAD_CREDS")
sys.exit(EXIT_BAD_CREDS)
logging.info(f"🔐 Testing login against: {service_url}")
logging.info(f"👤 Handle: {handle}")
# Optional but useful diagnostics
preflight_health(service_url)
client = build_client(service_url)
last_kind = "other"
for attempt in range(1, args.max_attempts + 1):
try:
logging.info(f"➡️ Login attempt {attempt}/{args.max_attempts}")
client.login(handle, app_password)
logging.info("✅ Login successful.")
print("LOGIN_OK")
sys.exit(EXIT_OK)
except Exception as e:
last_kind = classify_error(e)
logging.exception(f"❌ Login failed [{last_kind}]")
if last_kind == "bad_creds":
print("LOGIN_FAILED_BAD_CREDS")
sys.exit(EXIT_BAD_CREDS)
if attempt >= args.max_attempts:
break
if last_kind == "rate_limit":
wait_s = parse_wait_seconds_from_exception(e, default_delay=args.base_delay)
elif last_kind == "network":
wait_s = min(args.base_delay * attempt, 60)
else:
wait_s = min(args.base_delay * attempt, 45)
wait_s = wait_s + random.uniform(0, max(args.jitter_max, 0.0))
logging.warning(f"⏳ Waiting {wait_s:.1f}s before retry...")
time.sleep(wait_s)
if last_kind == "rate_limit":
print("LOGIN_FAILED_RATE_LIMIT")
sys.exit(EXIT_RATE_LIMIT)
if last_kind == "network":
print("LOGIN_FAILED_NETWORK")
sys.exit(EXIT_NETWORK)
print("LOGIN_FAILED")
sys.exit(EXIT_OTHER)
if __name__ == "__main__":
main()
main()

View File

@@ -11,6 +11,7 @@ import time
import os
import subprocess
import uuid
import random
from urllib.parse import urlparse
from dotenv import load_dotenv
from atproto import Client, client_utils, models
@@ -49,6 +50,12 @@ BSKY_SEND_POST_MAX_RETRIES = 3
BSKY_SEND_POST_BASE_DELAY = 5
BSKY_SEND_POST_MAX_DELAY = 60
# --- Login hardening (NEW) ---
BSKY_LOGIN_MAX_RETRIES = 4
BSKY_LOGIN_BASE_DELAY = 10
BSKY_LOGIN_MAX_DELAY = 600
BSKY_LOGIN_JITTER_MAX = 1.5
MEDIA_DOWNLOAD_TIMEOUT = 30
LINK_METADATA_TIMEOUT = 10
URL_RESOLVE_TIMEOUT = 12
@@ -1336,27 +1343,112 @@ def build_text_media_key(normalized_text, media_fingerprint):
).hexdigest()
# --- Login hardening helpers (NEW) ---
def is_rate_limited_error(error_obj):
text = repr(error_obj).lower()
return (
"429" in text
or "ratelimitexceeded" in text
or "too many requests" in text
or "rate limit" in text
)
def is_auth_error(error_obj):
text = repr(error_obj).lower()
return (
"401" in text
or "403" in text
or "invalid identifier or password" in text
or "authenticationrequired" in text
or "invalidtoken" in text
)
def is_network_error(error_obj):
text = repr(error_obj)
signals = [
"ConnectError",
"RemoteProtocolError",
"ReadTimeout",
"WriteTimeout",
"TimeoutException",
"503",
"502",
"504",
"ConnectionResetError",
]
return any(sig in text for sig in signals)
def create_bsky_client(base_url, handle, password):
normalized_base_url = (base_url or DEFAULT_BSKY_BASE_URL).strip().rstrip("/")
logging.info(f"🔐 Connecting Bluesky client via base URL: {normalized_base_url}")
try:
client = Client(base_url=normalized_base_url)
except TypeError:
logging.warning(
"⚠️ Your atproto Client does not accept base_url in constructor. Falling back."
)
client = Client()
try:
if hasattr(client, "base_url"):
client.base_url = normalized_base_url
elif hasattr(client, "_base_url"):
client._base_url = normalized_base_url
except Exception as e:
logging.warning(f"⚠️ Could not apply custom base URL cleanly: {e}")
client = Client(base_url=normalized_base_url)
client.login(handle, password)
return client
max_attempts = BSKY_LOGIN_MAX_RETRIES
base_delay = BSKY_LOGIN_BASE_DELAY
max_delay = BSKY_LOGIN_MAX_DELAY
jitter_max = max(BSKY_LOGIN_JITTER_MAX, 0.0)
for attempt in range(1, max_attempts + 1):
try:
logging.info(f"🔐 Bluesky login attempt {attempt}/{max_attempts} for {handle}")
client.login(handle, password)
logging.info("✅ Bluesky login successful.")
return client
except Exception as e:
logging.exception("❌ Bluesky login exception")
# Fail fast on invalid credentials
if is_auth_error(e):
logging.error("❌ Bluesky auth failed (invalid handle/app password).")
raise
# Respect explicit rate-limit timing
if is_rate_limited_error(e):
if attempt < max_attempts:
wait = get_rate_limit_wait_seconds(e, default_delay=base_delay)
wait = wait + random.uniform(0, jitter_max)
logging.warning(
f"⏳ Bluesky login rate-limited (attempt {attempt}/{max_attempts}). "
f"Retrying in {wait:.1f}s."
)
time.sleep(wait)
continue
logging.error("❌ Exhausted Bluesky login retries due to rate limiting.")
raise
# Retry transient/network problems
if is_network_error(e) or is_transient_error(e):
if attempt < max_attempts:
wait = min(base_delay * attempt, max_delay) + random.uniform(0, jitter_max)
logging.warning(
f"⏳ Transient Bluesky login failure (attempt {attempt}/{max_attempts}). "
f"Retrying in {wait:.1f}s."
)
time.sleep(wait)
continue
logging.error("❌ Exhausted Bluesky login retries after transient/network errors.")
raise
# Unknown errors: bounded retry anyway
if attempt < max_attempts:
wait = min(base_delay * attempt, max_delay) + random.uniform(0, jitter_max)
logging.warning(
f"⏳ Bluesky login retry for unexpected error "
f"(attempt {attempt}/{max_attempts}) in {wait:.1f}s."
)
time.sleep(wait)
continue
raise
raise RuntimeError("Bluesky login failed after all retries.")
# --- State Management ---
@@ -1573,20 +1665,70 @@ def get_recent_bsky_posts(client, handle, limit=30):
# --- Upload / Retry Helpers ---
def get_rate_limit_wait_seconds(error_obj, default_delay):
"""
Parse common rate-limit headers and return a bounded wait time in seconds.
Supports:
- retry-after
- x-ratelimit-after
- ratelimit-reset (unix timestamp)
"""
try:
headers = getattr(error_obj, "headers", None)
if headers:
reset_value = headers.get("ratelimit-reset") or headers.get(
"RateLimit-Reset"
)
if reset_value:
now_ts = int(time.time())
reset_ts = int(reset_value)
wait_seconds = max(reset_ts - now_ts + 1, default_delay)
return min(wait_seconds, BSKY_BLOB_UPLOAD_MAX_DELAY)
now_ts = int(time.time())
# Direct headers on exception
headers = getattr(error_obj, "headers", None) or {}
retry_after = headers.get("retry-after") or headers.get("Retry-After")
if retry_after:
return min(max(int(retry_after), 1), BSKY_LOGIN_MAX_DELAY)
x_after = headers.get("x-ratelimit-after") or headers.get("X-RateLimit-After")
if x_after:
return min(max(int(x_after), 1), BSKY_LOGIN_MAX_DELAY)
reset_value = headers.get("ratelimit-reset") or headers.get("RateLimit-Reset")
if reset_value:
wait_seconds = max(int(reset_value) - now_ts + 1, default_delay)
return min(wait_seconds, BSKY_LOGIN_MAX_DELAY)
except Exception:
pass
try:
# Nested response headers
response = getattr(error_obj, "response", None)
headers = getattr(response, "headers", None) or {}
now_ts = int(time.time())
retry_after = headers.get("retry-after") or headers.get("Retry-After")
if retry_after:
return min(max(int(retry_after), 1), BSKY_LOGIN_MAX_DELAY)
x_after = headers.get("x-ratelimit-after") or headers.get("X-RateLimit-After")
if x_after:
return min(max(int(x_after), 1), BSKY_LOGIN_MAX_DELAY)
reset_value = headers.get("ratelimit-reset") or headers.get("RateLimit-Reset")
if reset_value:
wait_seconds = max(int(reset_value) - now_ts + 1, default_delay)
return min(wait_seconds, BSKY_LOGIN_MAX_DELAY)
except Exception:
pass
# repr fallback parsing
text = repr(error_obj)
m = re.search(r"'retry-after': '(\d+)'", text, re.IGNORECASE)
if m:
return min(max(int(m.group(1)), 1), BSKY_LOGIN_MAX_DELAY)
m = re.search(r"'x-ratelimit-after': '(\d+)'", text, re.IGNORECASE)
if m:
return min(max(int(m.group(1)), 1), BSKY_LOGIN_MAX_DELAY)
m = re.search(r"'ratelimit-reset': '(\d+)'", text, re.IGNORECASE)
if m:
now_ts = int(time.time())
wait_seconds = max(int(m.group(1)) - now_ts + 1, default_delay)
return min(wait_seconds, BSKY_LOGIN_MAX_DELAY)
return default_delay