social-autoposter 1.3.7 → 1.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/scripts/post_reddit.py
CHANGED
|
@@ -449,13 +449,27 @@ def _ban_entries_to_subs(entries) -> set[str]:
|
|
|
449
449
|
|
|
450
450
|
|
|
451
451
|
def _make_ban_entry(sub: str, reason: str | None, project: str | None) -> dict:
|
|
452
|
-
"""Build a new ban-list entry with the current UTC timestamp.
|
|
452
|
+
"""Build a new ban-list entry with the current UTC timestamp.
|
|
453
|
+
|
|
454
|
+
Stamps the current Reddit account (top-level config.json reddit_account
|
|
455
|
+
.username) so per-account scoping in reddit_tools._load_comment_blocked_subs
|
|
456
|
+
can ignore this entry on other machines posting as a different account.
|
|
457
|
+
Returns account=None if the config has no reddit_account, in which case
|
|
458
|
+
the reader treats the entry as global (back-compat with pre-2026-05-15).
|
|
459
|
+
"""
|
|
453
460
|
from datetime import datetime, timezone
|
|
461
|
+
account = None
|
|
462
|
+
try:
|
|
463
|
+
with open(CONFIG_PATH) as _f:
|
|
464
|
+
account = (json.load(_f).get("reddit_account") or {}).get("username") or None
|
|
465
|
+
except Exception:
|
|
466
|
+
pass
|
|
454
467
|
return {
|
|
455
468
|
"sub": sub.strip().lower(),
|
|
456
469
|
"added_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
457
470
|
"reason": reason or None,
|
|
458
471
|
"project": project or None,
|
|
472
|
+
"account": account,
|
|
459
473
|
}
|
|
460
474
|
|
|
461
475
|
|
|
@@ -295,22 +295,52 @@ def _refresh_browser_lock():
|
|
|
295
295
|
|
|
296
296
|
|
|
297
297
|
def get_browser_and_page(playwright):
|
|
298
|
-
"""
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
298
|
+
"""Get a logged-in Reddit page, preferring CDP-attach over launch_persistent_context.
|
|
299
|
+
|
|
300
|
+
Two paths:
|
|
301
|
+
1. CDP-attach (preferred on appmaker/e2b VM and any host running a visible
|
|
302
|
+
logged-in Chromium): connect to the existing browser, find a context with
|
|
303
|
+
a live reddit_session cookie, open a NEW PAGE on that context.
|
|
304
|
+
2. launch_persistent_context fallback: when CDP isn't available OR contexts
|
|
305
|
+
have no reddit_session (laptop where reddit-agent MCP isolates its session
|
|
306
|
+
in an invisible context).
|
|
307
|
+
|
|
308
|
+
Why CDP-attach matters: appmaker's visible Chromium permanently holds
|
|
309
|
+
/root/.chromium-profile. launch_persistent_context collides on profile leveldb
|
|
310
|
+
locks, loads a partial session, and EVERY post returns account_blocked_in_sub
|
|
311
|
+
because the comment form never renders. Attaching to the live context dodges
|
|
312
|
+
the collision entirely.
|
|
313
|
+
|
|
314
|
+
Returns (browser, page, is_cdp). When is_cdp=True, callers must close ONLY
|
|
315
|
+
the page (not page.context) and NOT the browser; closing context[0] or the
|
|
316
|
+
CDP browser would kill the user's visible session.
|
|
307
317
|
"""
|
|
308
318
|
_acquire_browser_lock()
|
|
309
319
|
cdp_port = find_reddit_cdp_port()
|
|
310
320
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
321
|
+
if cdp_port:
|
|
322
|
+
try:
|
|
323
|
+
cdp_browser = playwright.chromium.connect_over_cdp(f"http://localhost:{cdp_port}")
|
|
324
|
+
for ctx in cdp_browser.contexts:
|
|
325
|
+
try:
|
|
326
|
+
cookies = ctx.cookies("https://www.reddit.com/")
|
|
327
|
+
except Exception:
|
|
328
|
+
cookies = []
|
|
329
|
+
has_session = any(
|
|
330
|
+
c.get("name") == "reddit_session" and c.get("value")
|
|
331
|
+
for c in cookies
|
|
332
|
+
)
|
|
333
|
+
if has_session:
|
|
334
|
+
page = ctx.new_page()
|
|
335
|
+
return cdp_browser, page, True
|
|
336
|
+
try:
|
|
337
|
+
cdp_browser.close()
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
except Exception:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
# Fallback: launch our own persistent context against PROFILE_DIR.
|
|
314
344
|
# Retry on Chromium SingletonLock collisions (MCP holds the OS-level profile
|
|
315
345
|
# lock for its entire server lifetime; the JSON lock can expire while the
|
|
316
346
|
# OS lock is still held).
|
|
@@ -496,9 +526,18 @@ def post_comment(thread_url, text):
|
|
|
496
526
|
}
|
|
497
527
|
|
|
498
528
|
finally:
|
|
499
|
-
|
|
529
|
+
try:
|
|
530
|
+
if is_cdp:
|
|
531
|
+
page.close()
|
|
532
|
+
else:
|
|
533
|
+
page.context.close()
|
|
534
|
+
except Exception:
|
|
535
|
+
pass
|
|
500
536
|
if not is_cdp:
|
|
501
|
-
|
|
537
|
+
try:
|
|
538
|
+
browser.close()
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
502
541
|
|
|
503
542
|
|
|
504
543
|
def reply_to_comment(comment_permalink, text, dm_id=None):
|
|
@@ -736,9 +775,18 @@ def reply_to_comment(comment_permalink, text, dm_id=None):
|
|
|
736
775
|
}
|
|
737
776
|
|
|
738
777
|
finally:
|
|
739
|
-
|
|
778
|
+
try:
|
|
779
|
+
if is_cdp:
|
|
780
|
+
page.close()
|
|
781
|
+
else:
|
|
782
|
+
page.context.close()
|
|
783
|
+
except Exception:
|
|
784
|
+
pass
|
|
740
785
|
if not is_cdp:
|
|
741
|
-
|
|
786
|
+
try:
|
|
787
|
+
browser.close()
|
|
788
|
+
except Exception:
|
|
789
|
+
pass
|
|
742
790
|
|
|
743
791
|
|
|
744
792
|
def edit_comment(comment_permalink, new_text):
|
|
@@ -859,9 +907,18 @@ def edit_comment(comment_permalink, new_text):
|
|
|
859
907
|
}
|
|
860
908
|
|
|
861
909
|
finally:
|
|
862
|
-
|
|
910
|
+
try:
|
|
911
|
+
if is_cdp:
|
|
912
|
+
page.close()
|
|
913
|
+
else:
|
|
914
|
+
page.context.close()
|
|
915
|
+
except Exception:
|
|
916
|
+
pass
|
|
863
917
|
if not is_cdp:
|
|
864
|
-
|
|
918
|
+
try:
|
|
919
|
+
browser.close()
|
|
920
|
+
except Exception:
|
|
921
|
+
pass
|
|
865
922
|
|
|
866
923
|
|
|
867
924
|
def edit_thread(thread_permalink, new_body):
|
|
@@ -956,9 +1013,18 @@ def edit_thread(thread_permalink, new_body):
|
|
|
956
1013
|
}
|
|
957
1014
|
|
|
958
1015
|
finally:
|
|
959
|
-
|
|
1016
|
+
try:
|
|
1017
|
+
if is_cdp:
|
|
1018
|
+
page.close()
|
|
1019
|
+
else:
|
|
1020
|
+
page.context.close()
|
|
1021
|
+
except Exception:
|
|
1022
|
+
pass
|
|
960
1023
|
if not is_cdp:
|
|
961
|
-
|
|
1024
|
+
try:
|
|
1025
|
+
browser.close()
|
|
1026
|
+
except Exception:
|
|
1027
|
+
pass
|
|
962
1028
|
|
|
963
1029
|
|
|
964
1030
|
def unread_dms():
|
|
@@ -1135,9 +1201,18 @@ def unread_dms():
|
|
|
1135
1201
|
return unique
|
|
1136
1202
|
|
|
1137
1203
|
finally:
|
|
1138
|
-
|
|
1204
|
+
try:
|
|
1205
|
+
if is_cdp:
|
|
1206
|
+
page.close()
|
|
1207
|
+
else:
|
|
1208
|
+
page.context.close()
|
|
1209
|
+
except Exception:
|
|
1210
|
+
pass
|
|
1139
1211
|
if not is_cdp:
|
|
1140
|
-
|
|
1212
|
+
try:
|
|
1213
|
+
browser.close()
|
|
1214
|
+
except Exception:
|
|
1215
|
+
pass
|
|
1141
1216
|
|
|
1142
1217
|
|
|
1143
1218
|
def read_conversation(chat_url, max_messages=20):
|
|
@@ -1293,9 +1368,18 @@ def read_conversation(chat_url, max_messages=20):
|
|
|
1293
1368
|
return result
|
|
1294
1369
|
|
|
1295
1370
|
finally:
|
|
1296
|
-
|
|
1371
|
+
try:
|
|
1372
|
+
if is_cdp:
|
|
1373
|
+
page.close()
|
|
1374
|
+
else:
|
|
1375
|
+
page.context.close()
|
|
1376
|
+
except Exception:
|
|
1377
|
+
pass
|
|
1297
1378
|
if not is_cdp:
|
|
1298
|
-
|
|
1379
|
+
try:
|
|
1380
|
+
browser.close()
|
|
1381
|
+
except Exception:
|
|
1382
|
+
pass
|
|
1299
1383
|
|
|
1300
1384
|
|
|
1301
1385
|
def _load_active_reddit_campaigns_for_dm():
|
|
@@ -1564,9 +1648,18 @@ def send_dm(chat_url, message, dm_id=None):
|
|
|
1564
1648
|
}
|
|
1565
1649
|
|
|
1566
1650
|
finally:
|
|
1567
|
-
|
|
1651
|
+
try:
|
|
1652
|
+
if is_cdp:
|
|
1653
|
+
page.close()
|
|
1654
|
+
else:
|
|
1655
|
+
page.context.close()
|
|
1656
|
+
except Exception:
|
|
1657
|
+
pass
|
|
1568
1658
|
if not is_cdp:
|
|
1569
|
-
|
|
1659
|
+
try:
|
|
1660
|
+
browser.close()
|
|
1661
|
+
except Exception:
|
|
1662
|
+
pass
|
|
1570
1663
|
|
|
1571
1664
|
|
|
1572
1665
|
def compose_dm(recipient, subject, body):
|
|
@@ -1859,9 +1952,18 @@ def compose_dm(recipient, subject, body):
|
|
|
1859
1952
|
return {"ok": True, "thread_url": page.url}
|
|
1860
1953
|
|
|
1861
1954
|
finally:
|
|
1862
|
-
|
|
1955
|
+
try:
|
|
1956
|
+
if is_cdp:
|
|
1957
|
+
page.close()
|
|
1958
|
+
else:
|
|
1959
|
+
page.context.close()
|
|
1960
|
+
except Exception:
|
|
1961
|
+
pass
|
|
1863
1962
|
if not is_cdp:
|
|
1864
|
-
|
|
1963
|
+
try:
|
|
1964
|
+
browser.close()
|
|
1965
|
+
except Exception:
|
|
1966
|
+
pass
|
|
1865
1967
|
|
|
1866
1968
|
|
|
1867
1969
|
def scrape_views(username, max_scrolls=300):
|
|
@@ -2022,9 +2124,18 @@ def scrape_views(username, max_scrolls=300):
|
|
|
2022
2124
|
except Exception as e:
|
|
2023
2125
|
return {"ok": False, "error": str(e)}
|
|
2024
2126
|
finally:
|
|
2025
|
-
|
|
2127
|
+
try:
|
|
2128
|
+
if is_cdp:
|
|
2129
|
+
page.close()
|
|
2130
|
+
else:
|
|
2131
|
+
page.context.close()
|
|
2132
|
+
except Exception:
|
|
2133
|
+
pass
|
|
2026
2134
|
if not is_cdp:
|
|
2027
|
-
|
|
2135
|
+
try:
|
|
2136
|
+
browser.close()
|
|
2137
|
+
except Exception:
|
|
2138
|
+
pass
|
|
2028
2139
|
|
|
2029
2140
|
|
|
2030
2141
|
def main():
|
package/scripts/reddit_tools.py
CHANGED
|
@@ -190,6 +190,13 @@ def _load_comment_blocked_subs(project_name=None):
|
|
|
190
190
|
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "config.json")
|
|
191
191
|
with open(config_path) as f:
|
|
192
192
|
config = json.load(f)
|
|
193
|
+
# Per-account scoping (2026-05-15): a ban applies only to the account
|
|
194
|
+
# that triggered it. Different machines may post the same project as
|
|
195
|
+
# different accounts (laptop=Deep_Ad1959, sandbox VM=StreetRefuse7512);
|
|
196
|
+
# without this filter, account A's real ban would suppress a sub for
|
|
197
|
+
# account B that has no such ban. Entries with account=null are
|
|
198
|
+
# treated as global (apply regardless), preserving pre-2026-05-15 data.
|
|
199
|
+
current_account = (config.get("reddit_account") or {}).get("username") or None
|
|
193
200
|
blocked = set()
|
|
194
201
|
bans = config.get("subreddit_bans") or {}
|
|
195
202
|
if isinstance(bans, dict):
|
|
@@ -198,8 +205,15 @@ def _load_comment_blocked_subs(project_name=None):
|
|
|
198
205
|
if not slug:
|
|
199
206
|
continue
|
|
200
207
|
entry_project = None
|
|
208
|
+
entry_account = None
|
|
201
209
|
if isinstance(entry, dict):
|
|
202
210
|
entry_project = entry.get("project") or None
|
|
211
|
+
entry_account = entry.get("account") or None
|
|
212
|
+
# Account filter first: if entry is tagged with a specific
|
|
213
|
+
# account and it's not the current one, this ban doesn't apply.
|
|
214
|
+
if (entry_account is not None and current_account is not None
|
|
215
|
+
and entry_account.lower() != current_account.lower()):
|
|
216
|
+
continue
|
|
203
217
|
if entry_project is None:
|
|
204
218
|
blocked.add(slug)
|
|
205
219
|
elif project_name and entry_project.lower() == project_name.lower():
|
|
@@ -214,7 +214,16 @@ def upsert_candidates(tweets, config, batch_id=None):
|
|
|
214
214
|
bookmarks_t1 = NULL,
|
|
215
215
|
t1_checked_at = NULL,
|
|
216
216
|
delta_score = NULL,
|
|
217
|
-
batch_id = COALESCE(
|
|
217
|
+
batch_id = COALESCE(twitter_candidates.batch_id, EXCLUDED.batch_id)
|
|
218
|
+
WHERE NOT (
|
|
219
|
+
twitter_candidates.status = 'pending'
|
|
220
|
+
AND twitter_candidates.batch_id IS DISTINCT FROM EXCLUDED.batch_id
|
|
221
|
+
AND EXISTS (
|
|
222
|
+
SELECT 1 FROM twitter_batches tb
|
|
223
|
+
WHERE tb.batch_id = twitter_candidates.batch_id
|
|
224
|
+
AND tb.phase_started_at > NOW() - INTERVAL '20 minutes'
|
|
225
|
+
)
|
|
226
|
+
)
|
|
218
227
|
""",
|
|
219
228
|
[
|
|
220
229
|
url,
|
|
@@ -117,7 +117,7 @@ def update_candidate(cid: int, status: str) -> None:
|
|
|
117
117
|
return
|
|
118
118
|
cmd = [
|
|
119
119
|
"psql", DATABASE_URL, "-c",
|
|
120
|
-
f"UPDATE twitter_candidates SET status='{sql_status}' WHERE id={cid}",
|
|
120
|
+
f"UPDATE twitter_candidates SET status='{sql_status}' WHERE id={cid} AND status != 'posted'",
|
|
121
121
|
]
|
|
122
122
|
rc, out, err = run_subprocess(cmd, timeout_sec=30)
|
|
123
123
|
if rc != 0:
|