Compare commits
28 Commits
344b2d56a3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baa055a36e | ||
|
|
6565c62a7a | ||
|
|
08cb7e18e3 | ||
|
|
bdeec32a25 | ||
|
|
4a83d526f9 | ||
|
|
48710862ab | ||
| 16c04ff7d3 | |||
| e3f1932775 | |||
| 05b69da411 | |||
|
|
bd79ddd40c | ||
|
|
d48646fba8 | ||
|
|
e7ac3c9b68 | ||
|
|
ffb0105a49 | ||
|
|
036b5483b3 | ||
|
|
3b0d3b6412 | ||
|
|
22b903f35b | ||
| 9fde78b048 | |||
|
|
2d7a86c2cf | ||
|
|
3b3edaf409 | ||
|
|
e4e1cf634e | ||
| 1f14a2cec4 | |||
| dc451475eb | |||
| 4a416d8468 | |||
| 4d04ba245a | |||
| e3e443c0c0 | |||
| 3fc74cb008 | |||
| 300d45192c | |||
| f11b0a1677 |
@@ -2,7 +2,7 @@ pipeline {
|
|||||||
agent any
|
agent any
|
||||||
|
|
||||||
options {
|
options {
|
||||||
timeout(time: 15, unit: 'MINUTES')
|
timeout(time: 20, unit: 'MINUTES')
|
||||||
timestamps()
|
timestamps()
|
||||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||||
disableConcurrentBuilds()
|
disableConcurrentBuilds()
|
||||||
@@ -12,10 +12,15 @@ pipeline {
|
|||||||
cron('H/30 * * * *')
|
cron('H/30 * * * *')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
VENV_DIR = 'venv'
|
||||||
|
PIP_CACHE_DIR = "${WORKSPACE}/.pip-cache"
|
||||||
|
PYTHONUNBUFFERED = '1'
|
||||||
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
stage('Checkout Code') {
|
stage('Checkout Code') {
|
||||||
steps {
|
steps {
|
||||||
// Pulls the code from the repository where this Jenkinsfile lives
|
|
||||||
checkout scm
|
checkout scm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,34 +28,41 @@ pipeline {
|
|||||||
stage('Setup Python & Install Dependencies') {
|
stage('Setup Python & Install Dependencies') {
|
||||||
steps {
|
steps {
|
||||||
sh '''
|
sh '''
|
||||||
set -e # Exit immediately if a command exits with a non-zero status
|
set -euxo pipefail
|
||||||
|
|
||||||
# Create a virtual environment named 'venv'
|
python3 -m venv "${VENV_DIR}"
|
||||||
python3 -m venv venv
|
|
||||||
|
|
||||||
# Activate the virtual environment and install dependencies
|
# Always use venv python explicitly
|
||||||
. venv/bin/activate && \
|
"${VENV_DIR}/bin/python" -m pip install --upgrade pip wheel setuptools
|
||||||
pip install --upgrade pip && \
|
|
||||||
pip install -U atproto tweety-ns playwright httpx arrow python-dotenv moviepy
|
|
||||||
|
|
||||||
# Check if moviepy is installed
|
"${VENV_DIR}/bin/pip" install --cache-dir "${PIP_CACHE_DIR}" -U \
|
||||||
pip list | grep moviepy || { echo 'MoviePy installation failed!'; exit 1; }
|
atproto \
|
||||||
|
tweety-ns \
|
||||||
|
playwright \
|
||||||
|
httpx \
|
||||||
|
arrow \
|
||||||
|
python-dotenv \
|
||||||
|
moviepy \
|
||||||
|
fastfeedparser \
|
||||||
|
beautifulsoup4 \
|
||||||
|
charset-normalizer \
|
||||||
|
Pillow
|
||||||
|
|
||||||
# Check if FFmpeg is installed
|
# Verify required imports
|
||||||
ffmpeg -version || { echo 'FFmpeg is not installed!'; exit 1; }
|
"${VENV_DIR}/bin/python" -c "import fastfeedparser; print('fastfeedparser OK')"
|
||||||
|
"${VENV_DIR}/bin/python" -c "import moviepy; print('moviepy OK')"
|
||||||
|
|
||||||
# Verify that moviepy can be imported
|
# Check FFmpeg
|
||||||
python3 -c "import moviepy" || { echo 'MoviePy import failed!'; exit 1; }
|
ffmpeg -version
|
||||||
|
|
||||||
# Install the local browser binaries for this environment
|
# Install Playwright browser binaries in this workspace environment
|
||||||
playwright install chromium
|
"${VENV_DIR}/bin/python" -m playwright install chromium
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Run Script') {
|
stage('Run Script') {
|
||||||
steps {
|
steps {
|
||||||
// Securely injects Jenkins credentials as environment variables
|
|
||||||
withCredentials([
|
withCredentials([
|
||||||
string(credentialsId: 'TWITTER_USERNAME', variable: 'TWITTER_USERNAME'),
|
string(credentialsId: 'TWITTER_USERNAME', variable: 'TWITTER_USERNAME'),
|
||||||
string(credentialsId: 'TWITTER_PASSWORD', variable: 'TWITTER_PASSWORD'),
|
string(credentialsId: 'TWITTER_PASSWORD', variable: 'TWITTER_PASSWORD'),
|
||||||
@@ -60,18 +72,26 @@ pipeline {
|
|||||||
string(credentialsId: 'BSKY_3CAT_APP_PASSWORD', variable: 'BSKY_3CAT_APP_PASSWORD')
|
string(credentialsId: 'BSKY_3CAT_APP_PASSWORD', variable: 'BSKY_3CAT_APP_PASSWORD')
|
||||||
]) {
|
]) {
|
||||||
sh '''
|
sh '''
|
||||||
# Activate the virtual environment and run the script
|
set -euxo pipefail
|
||||||
. venv/bin/activate && \
|
|
||||||
python3 twitter2bsky_daemon.py \
|
"${VENV_DIR}/bin/python" twitter2bsky_daemon.py \
|
||||||
--twitter-username "$TWITTER_USERNAME" \
|
--twitter-username "$TWITTER_USERNAME" \
|
||||||
--twitter-password "$TWITTER_PASSWORD" \
|
--twitter-password "$TWITTER_PASSWORD" \
|
||||||
--twitter-email "$TWITTER_3CAT_EMAIL" \
|
--twitter-email "$TWITTER_3CAT_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_3CAT_HANDLE" \
|
--twitter-handle "$TWITTER_3CAT_HANDLE" \
|
||||||
--bsky-handle "$BSKY_3CAT_HANDLE" \
|
--bsky-handle "$BSKY_3CAT_HANDLE" \
|
||||||
--bsky-password "$BSKY_3CAT_APP_PASSWORD"
|
--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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_FCB_EMAIL" \
|
--twitter-email "$TWITTER_FCB_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_FCBFUTBOLSALA_HANDLE" \
|
--twitter-handle "$TWITTER_FCBFUTBOLSALA_HANDLE" \
|
||||||
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
||||||
--bsky-password "$BSKY_MQUB_PASSWORD"
|
--bsky-password "$BSKY_MQUB_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_FCB_EMAIL" \
|
--twitter-email "$TWITTER_FCB_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_FCBHANDBOL_HANDLE" \
|
--twitter-handle "$TWITTER_FCBHANDBOL_HANDLE" \
|
||||||
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
||||||
--bsky-password "$BSKY_MQUB_PASSWORD"
|
--bsky-password "$BSKY_MQUB_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_FCB_EMAIL" \
|
--twitter-email "$TWITTER_FCB_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_FCBHOQUEI_HANDLE" \
|
--twitter-handle "$TWITTER_FCBHOQUEI_HANDLE" \
|
||||||
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
||||||
--bsky-password "$BSKY_MQUB_PASSWORD"
|
--bsky-password "$BSKY_MQUB_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_FCB_EMAIL" \
|
--twitter-email "$TWITTER_FCB_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_FCBMASIA_HANDLE" \
|
--twitter-handle "$TWITTER_FCBMASIA_HANDLE" \
|
||||||
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
||||||
--bsky-password "$BSKY_MQUB_PASSWORD"
|
--bsky-password "$BSKY_MQUB_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_APM_EMAIL" \
|
--twitter-email "$TWITTER_APM_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_APM_HANDLE" \
|
--twitter-handle "$TWITTER_APM_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_BETEVE_EMAIL" \
|
--twitter-email "$TWITTER_BETEVE_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_BETEVE_HANDLE" \
|
--twitter-handle "$TWITTER_BETEVE_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_BOMBERSCAT_HANDLE" \
|
--twitter-handle "$TWITTER_BOMBERSCAT_HANDLE" \
|
||||||
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
||||||
--bsky-password "$BSKY_CAT112_PASSWORD"
|
--bsky-password "$BSKY_CAT112_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_BETEVE_EMAIL" \
|
--twitter-email "$TWITTER_BETEVE_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_BTVESPORTS_HANDLE" \
|
--twitter-handle "$TWITTER_BTVESPORTS_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_BETEVE_EMAIL" \
|
--twitter-email "$TWITTER_BETEVE_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_BTVLLENGUA_HANDLE" \
|
--twitter-handle "$TWITTER_BTVLLENGUA_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_BETEVE_EMAIL" \
|
--twitter-email "$TWITTER_BETEVE_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_BTVNOTICIES_HANDLE" \
|
--twitter-handle "$TWITTER_BTVNOTICIES_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ pipeline {
|
|||||||
"https://cr-news-api-service.prd.crunchyrollsvc.com/v1/es-ES/rss" \
|
"https://cr-news-api-service.prd.crunchyrollsvc.com/v1/es-ES/rss" \
|
||||||
"$BSKY_CRUNCHYROLL_HANDLE" \
|
"$BSKY_CRUNCHYROLL_HANDLE" \
|
||||||
"$TWITTER_CRUNCHY_EMAIL" \
|
"$TWITTER_CRUNCHY_EMAIL" \
|
||||||
"$BSKY_CRUNCHYROLL_APP_PASSWORD"
|
"$BSKY_CRUNCHYROLL_APP_PASSWORD" \
|
||||||
|
--service "https://eurosky.social"
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_CRUNCHY_EMAIL" \
|
--twitter-email "$TWITTER_CRUNCHY_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_CRUNCHYROLL_HANDLE" \
|
--twitter-handle "$TWITTER_CRUNCHYROLL_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ pipeline {
|
|||||||
disableConcurrentBuilds()
|
disableConcurrentBuilds()
|
||||||
}
|
}
|
||||||
|
|
||||||
triggers {
|
triggers {
|
||||||
cron('''
|
cron('''
|
||||||
H 22,10,16 * * *
|
H 22,10,16 * * *
|
||||||
0 4 * * *
|
0 4 * * *
|
||||||
''')
|
''')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
environment {
|
environment {
|
||||||
VENV_DIR = 'venv'
|
VENV_DIR = 'venv'
|
||||||
@@ -114,7 +115,8 @@ H 22,10,16 * * *
|
|||||||
"${feedUrl}" \\
|
"${feedUrl}" \\
|
||||||
"\$BSKY_EP_HANDLE" \\
|
"\$BSKY_EP_HANDLE" \\
|
||||||
"\$BSKY_EP_USERNAME" \\
|
"\$BSKY_EP_USERNAME" \\
|
||||||
"\$BSKY_EP_APP_PASSWORD"
|
"\$BSKY_EP_APP_PASSWORD" \
|
||||||
|
--service "https://eurosky.social"
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_ELMONARAC1_HANDLE" \
|
--twitter-handle "$TWITTER_ELMONARAC1_HANDLE" \
|
||||||
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
||||||
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
|
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_EMERGENCIESCAT_HANDLE" \
|
--twitter-handle "$TWITTER_EMERGENCIESCAT_HANDLE" \
|
||||||
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
||||||
--bsky-password "$BSKY_CAT112_PASSWORD"
|
--bsky-password "$BSKY_CAT112_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_ESPORT3_EMAIL" \
|
--twitter-email "$TWITTER_ESPORT3_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_ESPORT3_HANDLE" \
|
--twitter-handle "$TWITTER_ESPORT3_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_ESPORTSRAC1_HANDLE" \
|
--twitter-handle "$TWITTER_ESPORTSRAC1_HANDLE" \
|
||||||
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
||||||
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
|
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_LAXARXAMES_EMAIL" \
|
--twitter-email "$TWITTER_LAXARXAMES_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_ESPORTENXARXA_HANDLE" \
|
--twitter-handle "$TWITTER_ESPORTENXARXA_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_FCBAC1_HANDLE" \
|
--twitter-handle "$TWITTER_FCBAC1_HANDLE" \
|
||||||
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
||||||
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
|
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_FCB_EMAIL" \
|
--twitter-email "$TWITTER_FCB_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_FCB_HANDLE" \
|
--twitter-handle "$TWITTER_FCB_HANDLE" \
|
||||||
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
||||||
--bsky-password "$BSKY_MQUB_PASSWORD"
|
--bsky-password "$BSKY_MQUB_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_FCB_EMAIL" \
|
--twitter-email "$TWITTER_FCB_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_FCBATLETIC_HANDLE" \
|
--twitter-handle "$TWITTER_FCBATLETIC_HANDLE" \
|
||||||
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
||||||
--bsky-password "$BSKY_MQUB_PASSWORD"
|
--bsky-password "$BSKY_MQUB_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_FCB_EMAIL" \
|
--twitter-email "$TWITTER_FCB_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_FCBBASKET_HANDLE" \
|
--twitter-handle "$TWITTER_FCBBASKET_HANDLE" \
|
||||||
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
||||||
--bsky-password "$BSKY_MQUB_PASSWORD"
|
--bsky-password "$BSKY_MQUB_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_FCB_EMAIL" \
|
--twitter-email "$TWITTER_FCB_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_FCBFEMENI_HANDLE" \
|
--twitter-handle "$TWITTER_FCBFEMENI_HANDLE" \
|
||||||
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
--bsky-handle "$BSKY_MQUB_HANDLE" \
|
||||||
--bsky-password "$BSKY_MQUB_PASSWORD"
|
--bsky-password "$BSKY_MQUB_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_LACOMPETENCIARAC1_HANDLE" \
|
--twitter-handle "$TWITTER_LACOMPETENCIARAC1_HANDLE" \
|
||||||
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
||||||
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
|
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_LAXARXAMES_EMAIL" \
|
--twitter-email "$TWITTER_LAXARXAMES_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_LAXARXAMES_HANDLE" \
|
--twitter-handle "$TWITTER_LAXARXAMES_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_METEORAC1_HANDLE" \
|
--twitter-handle "$TWITTER_METEORAC1_HANDLE" \
|
||||||
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
||||||
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
|
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_METEOCAT_EMAIL" \
|
--twitter-email "$TWITTER_METEOCAT_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_METEOCAT_HANDLE" \
|
--twitter-handle "$TWITTER_METEOCAT_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_MOSSOS_HANDLE" \
|
--twitter-handle "$TWITTER_MOSSOS_HANDLE" \
|
||||||
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
||||||
--bsky-password "$BSKY_CAT112_PASSWORD"
|
--bsky-password "$BSKY_CAT112_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_NOHOSERAC1_HANDLE" \
|
--twitter-handle "$TWITTER_NOHOSERAC1_HANDLE" \
|
||||||
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
||||||
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
|
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_OPTIMOT_EMAIL" \
|
--twitter-email "$TWITTER_OPTIMOT_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_OPTIMOT_HANDLE" \
|
--twitter-handle "$TWITTER_OPTIMOT_HANDLE" \
|
||||||
--bsky-handle "$BSKY_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
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_3CAT_EMAIL" \
|
--twitter-email "$TWITTER_3CAT_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_POLONIA3CAT_HANDLE" \
|
--twitter-handle "$TWITTER_POLONIA3CAT_HANDLE" \
|
||||||
--bsky-handle "$BSKY_3CAT_HANDLE" \
|
--bsky-handle "$BSKY_3CAT_HANDLE" \
|
||||||
--bsky-password "$BSKY_3CAT_APP_PASSWORD"
|
--bsky-password "$BSKY_3CAT_APP_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ pipeline {
|
|||||||
"https://www.rac1.cat/rss/home.xml" \\
|
"https://www.rac1.cat/rss/home.xml" \\
|
||||||
"$BSKY_RAC1_HANDLE" \\
|
"$BSKY_RAC1_HANDLE" \\
|
||||||
"$BSKY_RAC1_USERNAME" \\
|
"$BSKY_RAC1_USERNAME" \\
|
||||||
"$BSKY_RAC1_APP_PASSWORD"
|
"$BSKY_RAC1_APP_PASSWORD" \
|
||||||
|
--service "https://eurosky.social"
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_SEMGENCAT_HANDLE" \
|
--twitter-handle "$TWITTER_SEMGENCAT_HANDLE" \
|
||||||
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
||||||
--bsky-password "$BSKY_CAT112_PASSWORD"
|
--bsky-password "$BSKY_CAT112_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
--twitter-email "$TWITTER_CAT112_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_TRANSIT_HANDLE" \
|
--twitter-handle "$TWITTER_TRANSIT_HANDLE" \
|
||||||
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
--bsky-handle "$BSKY_CAT112_HANDLE" \
|
||||||
--bsky-password "$BSKY_CAT112_PASSWORD"
|
--bsky-password "$BSKY_CAT112_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_VERSIORAC1_HANDLE" \
|
--twitter-handle "$TWITTER_VERSIORAC1_HANDLE" \
|
||||||
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
||||||
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
|
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ pipeline {
|
|||||||
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
--twitter-email "$TWITTER_RAC1_EMAIL" \
|
||||||
--twitter-handle "$TWITTER_VIALLIURERAC1_HANDLE" \
|
--twitter-handle "$TWITTER_VIALLIURERAC1_HANDLE" \
|
||||||
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
--bsky-handle "$BSKY_RAC1_HANDLE" \
|
||||||
--bsky-password "$BSKY_RAC1_APP_PASSWORD"
|
--bsky-password "$BSKY_RAC1_APP_PASSWORD" \
|
||||||
|
--bsky-base-url https://eurosky.social
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
172
rss2bsky.py
172
rss2bsky.py
@@ -5,6 +5,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import httpx
|
import httpx
|
||||||
import time
|
import time
|
||||||
|
import random
|
||||||
import charset_normalizer
|
import charset_normalizer
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -55,6 +56,12 @@ class RetryConfig:
|
|||||||
blob_transient_error_delay: int = 10
|
blob_transient_error_delay: int = 10
|
||||||
post_retry_delay_seconds: int = 2
|
post_retry_delay_seconds: int = 2
|
||||||
|
|
||||||
|
# Login hardening
|
||||||
|
login_max_attempts: int = 5
|
||||||
|
login_base_delay_seconds: int = 2
|
||||||
|
login_max_delay_seconds: int = 600
|
||||||
|
login_jitter_seconds: float = 1.5
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CooldownConfig:
|
class CooldownConfig:
|
||||||
@@ -498,29 +505,59 @@ def make_rich(content: str):
|
|||||||
# Error helpers
|
# Error helpers
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def get_rate_limit_reset_timestamp(error_obj):
|
def get_rate_limit_reset_timestamp(error_obj):
|
||||||
|
# 1) direct headers
|
||||||
try:
|
try:
|
||||||
headers = getattr(error_obj, "headers", None)
|
headers = getattr(error_obj, "headers", None) or {}
|
||||||
if headers:
|
now_ts = int(time.time())
|
||||||
|
|
||||||
|
retry_after = headers.get("retry-after") or headers.get("Retry-After")
|
||||||
|
if retry_after:
|
||||||
|
return now_ts + int(retry_after)
|
||||||
|
|
||||||
|
x_after = headers.get("x-ratelimit-after") or headers.get("X-RateLimit-After")
|
||||||
|
if x_after:
|
||||||
|
return now_ts + int(x_after)
|
||||||
|
|
||||||
reset_value = headers.get("ratelimit-reset") or headers.get("RateLimit-Reset")
|
reset_value = headers.get("ratelimit-reset") or headers.get("RateLimit-Reset")
|
||||||
if reset_value:
|
if reset_value:
|
||||||
return int(reset_value)
|
return int(reset_value)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 2) headers nested in response
|
||||||
try:
|
try:
|
||||||
response = getattr(error_obj, "response", None)
|
response = getattr(error_obj, "response", None)
|
||||||
headers = getattr(response, "headers", None)
|
headers = getattr(response, "headers", None) or {}
|
||||||
if headers:
|
now_ts = int(time.time())
|
||||||
|
|
||||||
|
retry_after = headers.get("retry-after") or headers.get("Retry-After")
|
||||||
|
if retry_after:
|
||||||
|
return now_ts + int(retry_after)
|
||||||
|
|
||||||
|
x_after = headers.get("x-ratelimit-after") or headers.get("X-RateLimit-After")
|
||||||
|
if x_after:
|
||||||
|
return now_ts + int(x_after)
|
||||||
|
|
||||||
reset_value = headers.get("ratelimit-reset") or headers.get("RateLimit-Reset")
|
reset_value = headers.get("ratelimit-reset") or headers.get("RateLimit-Reset")
|
||||||
if reset_value:
|
if reset_value:
|
||||||
return int(reset_value)
|
return int(reset_value)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 3) fallback parse
|
||||||
text = repr(error_obj)
|
text = repr(error_obj)
|
||||||
match = re.search(r"'ratelimit-reset': '(\d+)'", text)
|
|
||||||
if match:
|
m = re.search(r"'retry-after': '(\d+)'", text, re.IGNORECASE)
|
||||||
return int(match.group(1))
|
if m:
|
||||||
|
return int(time.time()) + int(m.group(1))
|
||||||
|
|
||||||
|
m = re.search(r"'x-ratelimit-after': '(\d+)'", text, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return int(time.time()) + int(m.group(1))
|
||||||
|
|
||||||
|
m = re.search(r"'ratelimit-reset': '(\d+)'", text, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1))
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -532,7 +569,9 @@ def is_rate_limited_error(error_obj) -> bool:
|
|||||||
"429" in error_text or
|
"429" in error_text or
|
||||||
"429" in repr_text or
|
"429" in repr_text or
|
||||||
"RateLimitExceeded" in error_text or
|
"RateLimitExceeded" in error_text or
|
||||||
"RateLimitExceeded" in repr_text
|
"RateLimitExceeded" in repr_text or
|
||||||
|
"Too Many Requests" in error_text or
|
||||||
|
"Too Many Requests" in repr_text
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -559,6 +598,26 @@ def is_probable_length_error(exc) -> bool:
|
|||||||
return any(signal.lower() in text.lower() for signal in signals)
|
return any(signal.lower() in text.lower() for signal in signals)
|
||||||
|
|
||||||
|
|
||||||
|
def is_auth_error(error_obj) -> bool:
|
||||||
|
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) -> bool:
|
||||||
|
text = repr(error_obj)
|
||||||
|
signals = [
|
||||||
|
"ConnectError", "RemoteProtocolError", "ReadTimeout", "WriteTimeout",
|
||||||
|
"TimeoutException", "503", "502", "504", "ConnectionResetError"
|
||||||
|
]
|
||||||
|
return any(s in text for s in signals)
|
||||||
|
|
||||||
|
|
||||||
def activate_post_creation_cooldown_from_error(error_obj, cooldown_path: str, cfg: AppConfig) -> int:
|
def activate_post_creation_cooldown_from_error(error_obj, cooldown_path: str, cfg: AppConfig) -> int:
|
||||||
reset_ts = get_rate_limit_reset_timestamp(error_obj)
|
reset_ts = get_rate_limit_reset_timestamp(error_obj)
|
||||||
if not reset_ts:
|
if not reset_ts:
|
||||||
@@ -785,7 +844,7 @@ def compress_external_thumb_to_limit(image_bytes: bytes, cfg: AppConfig):
|
|||||||
img = img.resize(new_size, Image.LANCZOS)
|
img = img.resize(new_size, Image.LANCZOS)
|
||||||
logging.info(f"🖼️ Resized external thumb to {new_size[0]}x{new_size[1]}")
|
logging.info(f"🖼️ Resized external thumb to {new_size[0]}x{new_size[1]}")
|
||||||
|
|
||||||
best_so_far = None # explicit fix
|
best_so_far = None
|
||||||
|
|
||||||
for quality in [78, 70, 62, 54, 46, 40, cfg.limits.external_thumb_min_jpeg_quality]:
|
for quality in [78, 70, 62, 54, 46, 40, cfg.limits.external_thumb_min_jpeg_quality]:
|
||||||
out = io.BytesIO()
|
out = io.BytesIO()
|
||||||
@@ -997,20 +1056,75 @@ def build_candidates_from_feed(feed) -> List[EntryCandidate]:
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# Orchestration
|
# Orchestration
|
||||||
# ============================================================
|
# ============================================================
|
||||||
def login_with_backoff(client: Client, bsky_username: str, bsky_password: str, service_url: str):
|
def login_with_backoff(
|
||||||
backoff = 60
|
client: Client,
|
||||||
while True:
|
bsky_username: str,
|
||||||
try:
|
bsky_password: str,
|
||||||
if check_post_cooldown_or_log(args.cooldown_path):
|
service_url: str,
|
||||||
|
cooldown_path: str,
|
||||||
|
cfg: AppConfig
|
||||||
|
) -> bool:
|
||||||
|
if check_post_cooldown_or_log(cooldown_path):
|
||||||
return False
|
return False
|
||||||
logging.info(f"🔐 Attempting login to server: {service_url} with user: {bsky_username}")
|
|
||||||
|
max_attempts = cfg.retry.login_max_attempts
|
||||||
|
base_delay = cfg.retry.login_base_delay_seconds
|
||||||
|
max_delay = cfg.retry.login_max_delay_seconds
|
||||||
|
jitter_max = max(cfg.retry.login_jitter_seconds, 0.0)
|
||||||
|
|
||||||
|
for attempt in range(1, max_attempts + 1):
|
||||||
|
try:
|
||||||
|
if check_post_cooldown_or_log(cooldown_path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f"🔐 Attempting login to server: {service_url} "
|
||||||
|
f"with user: {bsky_username} (attempt {attempt}/{max_attempts})"
|
||||||
|
)
|
||||||
client.login(bsky_username, bsky_password)
|
client.login(bsky_username, bsky_password)
|
||||||
logging.info(f"✅ Login successful for user: {bsky_username}")
|
logging.info(f"✅ Login successful for user: {bsky_username}")
|
||||||
return True
|
return True
|
||||||
except Exception:
|
|
||||||
|
except Exception as e:
|
||||||
logging.exception("❌ Login exception")
|
logging.exception("❌ Login exception")
|
||||||
time.sleep(backoff)
|
|
||||||
backoff = min(backoff + 60, 600)
|
# Rate-limited login: retry first, cooldown only if exhausted
|
||||||
|
if is_rate_limited_error(e):
|
||||||
|
if attempt < max_attempts:
|
||||||
|
wait_seconds = get_rate_limit_wait_seconds(e, base_delay, cfg)
|
||||||
|
wait_seconds = min(wait_seconds, max_delay) + random.uniform(0, jitter_max)
|
||||||
|
logging.warning(
|
||||||
|
f"⏳ Login rate-limited. Retrying in {wait_seconds:.1f}s "
|
||||||
|
f"(attempt {attempt}/{max_attempts})"
|
||||||
|
)
|
||||||
|
time.sleep(wait_seconds)
|
||||||
|
continue
|
||||||
|
|
||||||
|
activate_post_creation_cooldown_from_error(e, cooldown_path, cfg)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Bad credentials: fail fast
|
||||||
|
if is_auth_error(e):
|
||||||
|
logging.error("❌ Authentication failed (bad handle/password/app-password).")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Network/transient: bounded retry
|
||||||
|
if attempt < max_attempts and (is_network_error(e) or is_timeout_error(e)):
|
||||||
|
delay = min(base_delay * attempt, max_delay) + random.uniform(0, jitter_max)
|
||||||
|
logging.warning(f"⏳ Transient login failure. Retrying in {delay:.1f}s...")
|
||||||
|
time.sleep(delay)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Other errors: bounded retry
|
||||||
|
if attempt < max_attempts:
|
||||||
|
delay = min(base_delay * attempt, max_delay) + random.uniform(0, jitter_max)
|
||||||
|
logging.warning(f"⏳ Login retry in {delay:.1f}s...")
|
||||||
|
time.sleep(delay)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def run_once(
|
def run_once(
|
||||||
@@ -1031,19 +1145,19 @@ def run_once(
|
|||||||
return RunResult(published_count=0, stopped_reason="global_post_cooldown_active")
|
return RunResult(published_count=0, stopped_reason="global_post_cooldown_active")
|
||||||
|
|
||||||
client = Client(base_url=service_url)
|
client = Client(base_url=service_url)
|
||||||
backoff = 60
|
|
||||||
while True:
|
logged_in = login_with_backoff(
|
||||||
try:
|
client=client,
|
||||||
|
bsky_username=bsky_username,
|
||||||
|
bsky_password=bsky_password,
|
||||||
|
service_url=service_url,
|
||||||
|
cooldown_path=cooldown_path,
|
||||||
|
cfg=cfg
|
||||||
|
)
|
||||||
|
if not logged_in:
|
||||||
if check_post_cooldown_or_log(cooldown_path):
|
if check_post_cooldown_or_log(cooldown_path):
|
||||||
return RunResult(published_count=0, stopped_reason="global_post_cooldown_active")
|
return RunResult(published_count=0, stopped_reason="global_post_cooldown_active")
|
||||||
logging.info(f"🔐 Attempting login to server: {service_url} with user: {bsky_username}")
|
return RunResult(published_count=0, stopped_reason="login_failed")
|
||||||
client.login(bsky_username, bsky_password)
|
|
||||||
logging.info(f"✅ Login successful for user: {bsky_username}")
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
logging.exception("❌ Login exception")
|
|
||||||
time.sleep(backoff)
|
|
||||||
backoff = min(backoff + 60, 600)
|
|
||||||
|
|
||||||
state = load_state(state_path)
|
state = load_state(state_path)
|
||||||
recent_bsky_posts = get_recent_bsky_posts(client, bsky_handle, limit=cfg.limits.dedupe_bsky_limit)
|
recent_bsky_posts = get_recent_bsky_posts(client, bsky_handle, limit=cfg.limits.dedupe_bsky_limit)
|
||||||
|
|||||||
212
testlogin.py
212
testlogin.py
@@ -1,46 +1,200 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import httpx
|
||||||
from atproto import Client
|
from atproto import Client
|
||||||
|
|
||||||
# --- Logging ---
|
# --- Logging ---
|
||||||
LOG_PATH = "rss2bsky_test.log"
|
LOG_PATH = "bsky_login_test.log"
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format="%(asctime)s %(message)s",
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
filename=LOG_PATH,
|
handlers=[
|
||||||
encoding="utf-8",
|
logging.FileHandler(LOG_PATH, encoding="utf-8"),
|
||||||
|
logging.StreamHandler(),
|
||||||
|
],
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
)
|
)
|
||||||
|
|
||||||
def main():
|
EXIT_OK = 0
|
||||||
# --- Parse command-line arguments ---
|
EXIT_BAD_CREDS = 2
|
||||||
parser = argparse.ArgumentParser(description="Post RSS to Bluesky.")
|
EXIT_RATE_LIMIT = 3
|
||||||
parser.add_argument("rss_feed", help="RSS feed URL")
|
EXIT_NETWORK = 4
|
||||||
parser.add_argument("bsky_handle", help="Bluesky handle")
|
EXIT_OTHER = 5
|
||||||
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
|
|
||||||
|
|
||||||
# --- Login ---
|
def parse_wait_seconds_from_exception(exc, default_delay=15, max_delay=900):
|
||||||
# SOLUCIÓ: Passem el base_url directament al constructor del Client
|
"""
|
||||||
client = Client(base_url=service_url)
|
Parse common rate-limit headers from atproto exceptions:
|
||||||
|
- retry-after (seconds)
|
||||||
backoff = 60
|
- x-ratelimit-after (seconds)
|
||||||
while True:
|
- ratelimit-reset (unix timestamp)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
logging.info(f"Attempting login to server: {service_url} with user: {bsky_username}")
|
headers = getattr(exc, "headers", None) or {}
|
||||||
client.login(bsky_username, bsky_password)
|
|
||||||
logging.info(f"Login successful for user: {bsky_username}")
|
retry_after = headers.get("retry-after") or headers.get("Retry-After")
|
||||||
break
|
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:
|
except Exception as e:
|
||||||
logging.exception("Login exception")
|
logging.warning(f"🩺 Health check failed: {e}")
|
||||||
time.sleep(backoff)
|
return False
|
||||||
backoff = min(backoff + 60, 600)
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
if hasattr(c, "base_url"):
|
||||||
|
c.base_url = normalized
|
||||||
|
elif hasattr(c, "_base_url"):
|
||||||
|
c._base_url = normalized
|
||||||
|
except Exception as e:
|
||||||
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -11,6 +11,7 @@ import time
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import uuid
|
import uuid
|
||||||
|
import random
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from atproto import Client, client_utils, models
|
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_BASE_DELAY = 5
|
||||||
BSKY_SEND_POST_MAX_DELAY = 60
|
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
|
MEDIA_DOWNLOAD_TIMEOUT = 30
|
||||||
LINK_METADATA_TIMEOUT = 10
|
LINK_METADATA_TIMEOUT = 10
|
||||||
URL_RESOLVE_TIMEOUT = 12
|
URL_RESOLVE_TIMEOUT = 12
|
||||||
@@ -1336,28 +1343,113 @@ def build_text_media_key(normalized_text, media_fingerprint):
|
|||||||
).hexdigest()
|
).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):
|
def create_bsky_client(base_url, handle, password):
|
||||||
normalized_base_url = (base_url or DEFAULT_BSKY_BASE_URL).strip().rstrip("/")
|
normalized_base_url = (base_url or DEFAULT_BSKY_BASE_URL).strip().rstrip("/")
|
||||||
logging.info(f"🔐 Connecting Bluesky client via base URL: {normalized_base_url}")
|
logging.info(f"🔐 Connecting Bluesky client via base URL: {normalized_base_url}")
|
||||||
|
|
||||||
try:
|
|
||||||
client = Client(base_url=normalized_base_url)
|
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}")
|
|
||||||
|
|
||||||
|
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)
|
client.login(handle, password)
|
||||||
|
logging.info("✅ Bluesky login successful.")
|
||||||
return client
|
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 ---
|
# --- State Management ---
|
||||||
def default_state():
|
def default_state():
|
||||||
@@ -1573,20 +1665,70 @@ def get_recent_bsky_posts(client, handle, limit=30):
|
|||||||
|
|
||||||
# --- Upload / Retry Helpers ---
|
# --- Upload / Retry Helpers ---
|
||||||
def get_rate_limit_wait_seconds(error_obj, default_delay):
|
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:
|
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())
|
now_ts = int(time.time())
|
||||||
reset_ts = int(reset_value)
|
|
||||||
wait_seconds = max(reset_ts - now_ts + 1, default_delay)
|
# Direct headers on exception
|
||||||
return min(wait_seconds, BSKY_BLOB_UPLOAD_MAX_DELAY)
|
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:
|
except Exception:
|
||||||
pass
|
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
|
return default_delay
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user