···
1
1
import os
2
2
-
import json
2
2
+
import sys
3
3
+
import subprocess
3
4
import logging
5
5
+
import json
4
6
import requests
5
7
import magic
6
8
from datetime import datetime
7
9
from dotenv import load_dotenv
8
10
from atproto import Client, models
9
11
from atproto.exceptions import BadRequestError
10
10
-
import sys
11
12
13
13
+
# Ensure the script is run inside a virtual environment
12
14
if not hasattr(sys, 'real_prefix') and not (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
13
15
print("Error: This script must be run inside a virtual environment.")
14
16
sys.exit(1)
15
17
else:
16
18
print("Virtual environment detected.")
17
19
18
18
-
# Define the paths
19
19
-
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
20
20
+
# Define paths
21
21
+
BASE_DIR = os.path.abspath(os.path.dirname(__file__)) # /src/
22
22
+
REQ_PATH = os.path.abspath(os.path.join(BASE_DIR, "../requirements.txt")) # /requirements.txt
20
23
ASSETS_DIR = os.path.join(BASE_DIR, "../assets")
21
24
ENV_PATH = os.path.join(ASSETS_DIR, ".env")
22
25
JSON_PATH = os.path.join(ASSETS_DIR, "cids.json")
26
26
+
LOG_PATH = os.path.join(BASE_DIR, "avatar_update.log")
23
27
24
28
# Configure logging
25
29
logging.basicConfig(
26
26
-
filename="avatar_update.log",
30
30
+
filename=LOG_PATH,
27
31
level=logging.DEBUG,
28
32
format="%(asctime)s - %(levelname)s - %(message)s",
29
33
)
···
34
38
logging.getLogger().addHandler(console_handler)
35
39
36
40
41
41
+
def install_and_rerun():
42
42
+
"""Install missing packages from requirements.txt and re-run the script."""
43
43
+
if os.path.exists(REQ_PATH):
44
44
+
logging.info(f"Installing missing packages from {REQ_PATH}...")
45
45
+
try:
46
46
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", REQ_PATH])
47
47
+
logging.info("Packages installed successfully. Restarting script...")
48
48
+
os.execv(sys.executable, [sys.executable] + sys.argv)
49
49
+
except subprocess.CalledProcessError as e:
50
50
+
logging.error(f"Failed to install packages: {e}")
51
51
+
sys.exit(1)
52
52
+
else:
53
53
+
logging.error(f"requirements.txt not found at {REQ_PATH}, cannot install missing packages.")
54
54
+
sys.exit(1)
55
55
+
56
56
+
57
57
+
# Try to import external packages; if any are missing, install them.
58
58
+
try:
59
59
+
import requests
60
60
+
import magic
61
61
+
from atproto import Client, models
62
62
+
from atproto.exceptions import BadRequestError
63
63
+
from dotenv import load_dotenv
64
64
+
except ImportError as e:
65
65
+
logging.error(f"Missing package(s): {e}")
66
66
+
install_and_rerun()
67
67
+
68
68
+
37
69
def ensure_https(url):
70
70
+
"""Ensure the URL starts with https://"""
38
71
if not url.startswith("http://") and not url.startswith("https://"):
39
72
return "https://" + url
40
73
if url.startswith("http://"):
···
43
76
44
77
45
78
def is_endpoint_alive(url):
79
79
+
"""Check if the endpoint is alive by making a health check request."""
46
80
health_url = f"{url.rstrip('/')}/xrpc/_health"
81
81
+
logging.info(f"Checking endpoint health: {health_url}")
47
82
try:
48
83
response = requests.get(health_url, timeout=5)
49
49
-
return response.status_code == 200
84
84
+
if response.status_code == 200:
85
85
+
logging.info("Endpoint is alive.")
86
86
+
return True
87
87
+
else:
88
88
+
logging.warning(f"Endpoint returned status code {response.status_code}")
89
89
+
return False
50
90
except requests.RequestException as e:
51
91
logging.error(f"Health check failed for {health_url}: {e}")
52
92
return False
53
93
54
94
55
95
def fetch_blob(did, cid, endpoint):
96
96
+
"""Fetch blob data from the given endpoint."""
56
97
url = f"{endpoint}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}"
98
98
+
logging.info(f"Fetching blob: {url}")
57
99
try:
58
100
response = requests.get(url, timeout=5)
59
101
response.raise_for_status()
102
102
+
logging.info(f"Successfully fetched blob {cid} for DID {did}.")
60
103
return response.content
61
104
except requests.RequestException as e:
62
105
logging.error(f"Failed to fetch blob {cid} for DID {did}: {e}")
···
64
107
65
108
66
109
def get_blob_metadata(cid, did, endpoint):
110
110
+
"""Retrieve metadata for a given blob CID."""
67
111
blob_data = fetch_blob(did, cid, endpoint)
68
112
if blob_data is None:
113
113
+
logging.error("Blob data is empty.")
69
114
return None
70
115
71
116
mime = magic.Magic(mime=True)
72
117
mime_type = mime.from_buffer(blob_data)
73
118
size = len(blob_data)
74
119
120
120
+
logging.info(f"Retrieved metadata - MIME Type: {mime_type}, Size: {size} bytes")
121
121
+
75
122
return {
76
123
"$type": "blob",
77
124
"ref": {"$link": cid},
···
79
126
"size": size,
80
127
}
81
128
129
129
+
130
130
+
def setup_cron_job():
131
131
+
"""Set up a cron job to run the script hourly."""
132
132
+
cron_job_command = f"0 * * * * {sys.executable} {os.path.abspath(__file__)} >> {LOG_PATH} 2>&1"
133
133
+
logging.info("Setting up cron job...")
134
134
+
135
135
+
try:
136
136
+
result = subprocess.run(["crontab", "-l"], capture_output=True, text=True)
137
137
+
cron_jobs = result.stdout.strip() if result.returncode == 0 else ""
138
138
+
139
139
+
if cron_job_command in cron_jobs:
140
140
+
logging.info("Cron job is already set.")
141
141
+
return
142
142
+
143
143
+
new_cron_jobs = cron_jobs + "\n" + cron_job_command if cron_jobs else cron_job_command
144
144
+
subprocess.run(["crontab"], input=new_cron_jobs, text=True, check=True)
145
145
+
logging.info("Cron job added successfully.")
146
146
+
except Exception as e:
147
147
+
logging.error(f"Failed to set up cron job: {e}")
148
148
+
149
149
+
82
150
def main():
83
151
logging.info("Starting avatar update script...")
84
152
85
153
if os.path.exists(ENV_PATH):
86
154
load_dotenv(ENV_PATH)
155
155
+
logging.info("Loaded .env file successfully.")
87
156
else:
88
157
logging.error(f"Missing .env file at {ENV_PATH}")
89
158
return
···
94
163
did = os.getenv("DID")
95
164
96
165
if not (endpoint and handle and password and did):
97
97
-
logging.error(
98
98
-
"Missing environment variables. Ensure ENDPOINT, HANDLE, PASSWORD, and DID are set in .env file."
99
99
-
)
166
166
+
logging.error("Missing environment variables. Check .env file.")
100
167
return
101
168
102
169
endpoint = ensure_https(endpoint)
···
129
196
logging.error(f"Authentication failed: {e}")
130
197
return
131
198
132
132
-
try:
133
133
-
current_profile_record = client.app.bsky.actor.profile.get(
134
134
-
client.me.did, "self"
135
135
-
)
136
136
-
current_profile = current_profile_record.value
137
137
-
swap_record_cid = current_profile_record.cid
138
138
-
except BadRequestError:
139
139
-
current_profile = swap_record_cid = None
140
140
-
141
141
-
old_description = old_display_name = None
142
142
-
if current_profile:
143
143
-
old_description = current_profile.description
144
144
-
old_display_name = current_profile.display_name
145
145
-
146
199
blob_metadata = get_blob_metadata(new_blob_cid, did, endpoint)
147
147
-
148
200
if blob_metadata is None:
149
149
-
logging.error(f"Could not retrieve metadata for blob CID: {new_blob_cid}")
201
201
+
logging.error("Blob metadata retrieval failed.")
150
202
return
151
203
152
204
try:
···
155
207
collection=models.ids.AppBskyActorProfile,
156
208
repo=client.me.did,
157
209
rkey="self",
158
158
-
swap_record=swap_record_cid,
159
210
record=models.AppBskyActorProfile.Record(
160
211
avatar=blob_metadata,
161
161
-
banner=current_profile.banner if current_profile else None,
162
162
-
description=old_description,
163
163
-
display_name=old_display_name,
164
212
),
165
213
)
166
214
)
···
170
218
171
219
172
220
if __name__ == "__main__":
221
221
+
setup_cron_job()
173
222
main()
223
223
+