+61
-37
tools/bluesky/create_bluesky_post.py
+61
-37
tools/bluesky/create_bluesky_post.py
···
187
raise
188
189
190
-
def _verify_consent(client, agent_did: str, agent_handle: str, reply_to_uri: str):
191
"""
192
Orchestrates the consent checks.
193
Raises Exception with specific message if consent denied or verification fails.
194
"""
195
try:
196
-
# 0. Get target DID from reply_to_uri
197
-
# We need to fetch the post we are replying to to get its author
198
-
# We can do this via get_post_thread or get_posts.
199
-
# Since we need thread later, maybe we just do that?
200
-
# But Follow Check is cheaper than Thread Check.
201
-
# So we should get the post details first (1 API call).
202
-
203
-
# Parse DID from URI first to avoid API call if possible?
204
-
# URI format: at://did:plc:xyz/...
205
-
# The DID in the URI is the REPO owner, which is usually the author.
206
-
parts = reply_to_uri.replace('at://', '').split('/')
207
-
if len(parts) >= 1:
208
-
target_did = parts[0]
209
-
else:
210
-
# Should have been caught by validation, but just in case
211
-
raise Exception("Invalid URI format")
212
-
213
# Check 1: Self-Post
214
if _check_is_self(agent_did, target_did):
215
return True
216
217
-
# Check 2: Follow Check
218
-
if _check_follows(client, agent_did, target_did):
219
return True
220
221
-
# Check 3 & 4: Thread Participation / Mention
222
-
# This requires fetching the thread
223
if _check_thread_participation(client, agent_did, agent_handle, reply_to_uri):
224
return True
225
···
326
client = Client()
327
client.login(username, password)
328
329
-
# --- CONSENT GUARDRAILS ---
330
-
if reply_to_uri:
331
-
try:
332
-
agent_did = client.me.did
333
-
# agent_handle is username (without @ usually, but let's ensure)
334
-
agent_handle = username.replace('@', '')
335
-
336
-
_verify_consent(client, agent_did, agent_handle, reply_to_uri)
337
-
except Exception as e:
338
-
return {
339
-
"status": "error",
340
-
"message": str(e)
341
-
}
342
-
# --------------------------
343
-
344
initial_reply_ref = None
345
initial_root_ref = None
346
347
if reply_to_uri:
348
try:
···
363
"status": "error",
364
"message": f"Could not retrieve post data from URI: {reply_to_uri}. The post may not exist or the URI may be incorrect."
365
}
366
367
parent_ref = models.ComAtprotoRepoStrongRef.Main(
368
uri=parent_post.uri,
···
382
root=root_ref
383
)
384
initial_root_ref = root_ref
385
386
except Exception as e:
387
return {
388
"status": "error",
389
"message": f"Failed to fetch post to reply to: {str(e)}. Check the URI format and try again."
390
}
391
392
post_urls = []
393
previous_post_ref = None
···
187
raise
188
189
190
+
def _verify_consent(client, agent_did: str, agent_handle: str, reply_to_uri: str, target_did: str, root_did: str, parent_post_record=None):
191
"""
192
Orchestrates the consent checks.
193
Raises Exception with specific message if consent denied or verification fails.
194
"""
195
try:
196
# Check 1: Self-Post
197
if _check_is_self(agent_did, target_did):
198
return True
199
200
+
# Check 2: Mention Check (Free/Cheap)
201
+
# If the post we are replying to mentions us, we can reply.
202
+
if parent_post_record:
203
+
# Check facets for mention
204
+
if hasattr(parent_post_record, 'facets') and parent_post_record.facets:
205
+
for facet in parent_post_record.facets:
206
+
for feature in facet.features:
207
+
if hasattr(feature, 'did') and feature.did == agent_did:
208
+
return True
209
+
210
+
# Fallback: Check text for handle
211
+
if hasattr(parent_post_record, 'text') and f"@{agent_handle}" in parent_post_record.text:
212
+
return True
213
+
214
+
# Check 3: Follow Check
215
+
# Rule: Target must follow agent.
216
+
# Rule 3B: If root author is different from target, Root must ALSO follow agent.
217
+
218
+
target_follows = _check_follows(client, agent_did, target_did)
219
+
220
+
if target_follows:
221
+
# If target follows, we must also check root if it's different
222
+
if root_did and root_did != target_did and root_did != agent_did:
223
+
root_follows = _check_follows(client, agent_did, root_did)
224
+
if not root_follows:
225
+
# Target follows, but Root does not. Fail.
226
+
raise Exception(
227
+
"Message not sent: the author of the post follows you, but the thread starter (root author) "
228
+
"does not. We respect the consent of the thread owner."
229
+
)
230
return True
231
232
+
# Check 4: Thread Participation
233
+
# This requires fetching the thread (Expensive)
234
if _check_thread_participation(client, agent_did, agent_handle, reply_to_uri):
235
return True
236
···
337
client = Client()
338
client.login(username, password)
339
340
+
# --- FETCH PARENT/ROOT REFS ---
341
initial_reply_ref = None
342
initial_root_ref = None
343
+
target_did = None
344
+
root_did = None
345
+
parent_post_record = None
346
347
if reply_to_uri:
348
try:
···
363
"status": "error",
364
"message": f"Could not retrieve post data from URI: {reply_to_uri}. The post may not exist or the URI may be incorrect."
365
}
366
+
367
+
# Extract target DID from parent post
368
+
target_did = repo_did
369
+
parent_post_record = parent_post.value
370
371
parent_ref = models.ComAtprotoRepoStrongRef.Main(
372
uri=parent_post.uri,
···
386
root=root_ref
387
)
388
initial_root_ref = root_ref
389
+
390
+
# Extract root DID
391
+
root_uri_parts = root_ref.uri.replace('at://', '').split('/')
392
+
if len(root_uri_parts) >= 1:
393
+
root_did = root_uri_parts[0]
394
395
except Exception as e:
396
return {
397
"status": "error",
398
"message": f"Failed to fetch post to reply to: {str(e)}. Check the URI format and try again."
399
}
400
+
401
+
# --- CONSENT GUARDRAILS ---
402
+
if reply_to_uri:
403
+
try:
404
+
agent_did = client.me.did
405
+
# agent_handle is username (without @ usually, but let's ensure)
406
+
agent_handle = username.replace('@', '')
407
+
408
+
_verify_consent(client, agent_did, agent_handle, reply_to_uri, target_did, root_did, parent_post_record)
409
+
except Exception as e:
410
+
return {
411
+
"status": "error",
412
+
"message": str(e)
413
+
}
414
+
# --------------------------
415
416
post_urls = []
417
previous_post_ref = None