@m13v/s4l 1.6.197-rc.10
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/README.md +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1336 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +513 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- package/skill/topics.sh +51 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Refresh Instagram Graph API long-lived tokens before they expire.
|
|
3
|
+
|
|
4
|
+
Instagram long-lived user tokens are valid for ~60 days. Each call to the
|
|
5
|
+
refresh_access_token endpoint extends the lifetime by another 60 days. The
|
|
6
|
+
token must be at least 24 hours old to be refreshable, and Meta recommends
|
|
7
|
+
refreshing well before expiry (we use a 14-day buffer).
|
|
8
|
+
|
|
9
|
+
This script:
|
|
10
|
+
1. Iterates over every account in config.json -> instagram.accounts[].
|
|
11
|
+
2. Reads the current token + expiry from ~/instagram-graph-api/.env via the
|
|
12
|
+
ig_long_token_env / derived IG_TOKEN_EXPIRES_<suffix> key.
|
|
13
|
+
3. If the token expires within REFRESH_BUFFER_DAYS, calls the Graph API
|
|
14
|
+
refresh_access_token endpoint and rewrites the .env file in place
|
|
15
|
+
(atomic: write to tempfile then os.replace).
|
|
16
|
+
4. Prints a machine-readable SUMMARY line for the wrapper to log via
|
|
17
|
+
scripts/log_run.py.
|
|
18
|
+
|
|
19
|
+
The .env file is the SINGLE source of truth — update_instagram_stats.py and
|
|
20
|
+
scan_instagram_comments.py both read it on every invocation, so a refreshed
|
|
21
|
+
token is picked up by the next pipeline run with no daemon-restart needed.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
python3 scripts/refresh_instagram_tokens.py [--quiet] [--force] [--dry-run]
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
import tempfile
|
|
34
|
+
import time
|
|
35
|
+
import urllib.error
|
|
36
|
+
import urllib.parse
|
|
37
|
+
import urllib.request
|
|
38
|
+
from datetime import datetime, timezone
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
IG_ENV_PATH = Path.home() / "instagram-graph-api" / ".env"
|
|
42
|
+
GRAPH = "https://graph.instagram.com"
|
|
43
|
+
SA_CONFIG = Path(__file__).resolve().parent.parent / "config.json"
|
|
44
|
+
|
|
45
|
+
# Refresh tokens that expire within this many days. 14 days gives us 2 weeks
|
|
46
|
+
# of headroom for cron failures, network outages, or attention lapses.
|
|
47
|
+
REFRESH_BUFFER_DAYS = 14
|
|
48
|
+
# Meta requires tokens to be at least 24h old before they can be refreshed.
|
|
49
|
+
MIN_TOKEN_AGE_HOURS = 24
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_env_lines() -> list[str]:
|
|
53
|
+
"""Return the .env file as a list of raw lines (preserving comments +
|
|
54
|
+
blank lines), so we can rewrite individual keys without reformatting."""
|
|
55
|
+
if not IG_ENV_PATH.exists():
|
|
56
|
+
return []
|
|
57
|
+
return IG_ENV_PATH.read_text().splitlines()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def env_dict_from_lines(lines: list[str]) -> dict[str, str]:
|
|
61
|
+
env = {}
|
|
62
|
+
for line in lines:
|
|
63
|
+
s = line.strip()
|
|
64
|
+
if not s or s.startswith("#") or "=" not in s:
|
|
65
|
+
continue
|
|
66
|
+
k, v = s.split("=", 1)
|
|
67
|
+
env[k.strip()] = v.strip()
|
|
68
|
+
return env
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def write_env_atomic(lines: list[str]):
|
|
72
|
+
"""Rewrite the .env file from `lines`. Atomic via temp-file + os.replace
|
|
73
|
+
so a Ctrl-C or crash mid-write can't truncate the file."""
|
|
74
|
+
dir_ = IG_ENV_PATH.parent
|
|
75
|
+
dir_.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
fd, tmp = tempfile.mkstemp(prefix=".env.tmp.", dir=str(dir_))
|
|
77
|
+
try:
|
|
78
|
+
with os.fdopen(fd, "w") as f:
|
|
79
|
+
f.write("\n".join(lines))
|
|
80
|
+
if lines and not lines[-1].endswith("\n"):
|
|
81
|
+
f.write("\n")
|
|
82
|
+
os.chmod(tmp, 0o600)
|
|
83
|
+
os.replace(tmp, IG_ENV_PATH)
|
|
84
|
+
finally:
|
|
85
|
+
if os.path.exists(tmp):
|
|
86
|
+
try:
|
|
87
|
+
os.unlink(tmp)
|
|
88
|
+
except OSError:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def expires_key_for(token_key: str) -> str:
|
|
93
|
+
"""Derive the IG_TOKEN_EXPIRES env-var name from the IG_LONG_TOKEN one.
|
|
94
|
+
|
|
95
|
+
IG_LONG_TOKEN -> IG_TOKEN_EXPIRES
|
|
96
|
+
IG_LONG_TOKEN_MATTHEWHEARTFUL -> IG_TOKEN_EXPIRES_MATTHEWHEARTFUL
|
|
97
|
+
IG_LONG_TOKEN_OMIDOTME -> IG_TOKEN_EXPIRES_OMIDOTME
|
|
98
|
+
"""
|
|
99
|
+
if not token_key.startswith("IG_LONG_TOKEN"):
|
|
100
|
+
return ""
|
|
101
|
+
return "IG_TOKEN_EXPIRES" + token_key[len("IG_LONG_TOKEN"):]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_expires(s: str | None) -> datetime | None:
|
|
105
|
+
if not s:
|
|
106
|
+
return None
|
|
107
|
+
s = s.strip()
|
|
108
|
+
if not s:
|
|
109
|
+
return None
|
|
110
|
+
# Accept both "2026-07-05T23:06:44Z" and "2026-07-05T23:06:44+00:00".
|
|
111
|
+
try:
|
|
112
|
+
if s.endswith("Z"):
|
|
113
|
+
s = s[:-1] + "+00:00"
|
|
114
|
+
return datetime.fromisoformat(s)
|
|
115
|
+
except ValueError:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def format_expires(dt: datetime) -> str:
|
|
120
|
+
"""Match the existing .env convention: ISO-8601 UTC with trailing Z."""
|
|
121
|
+
return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def refresh_token(long_token: str) -> dict:
|
|
125
|
+
qs = urllib.parse.urlencode({
|
|
126
|
+
"grant_type": "ig_refresh_token",
|
|
127
|
+
"access_token": long_token,
|
|
128
|
+
})
|
|
129
|
+
url = f"{GRAPH}/refresh_access_token?{qs}"
|
|
130
|
+
try:
|
|
131
|
+
with urllib.request.urlopen(url, timeout=30) as r:
|
|
132
|
+
return json.loads(r.read())
|
|
133
|
+
except urllib.error.HTTPError as e:
|
|
134
|
+
body = e.read().decode(errors="replace")
|
|
135
|
+
raise RefreshError(f"HTTP {e.code}: {body[:300]}") from e
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class RefreshError(Exception):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def update_line(lines: list[str], key: str, value: str) -> list[str]:
|
|
143
|
+
"""Return a new list with the line `<key>=<old>` replaced by `<key>=<value>`.
|
|
144
|
+
If the key isn't present, appends `<key>=<value>` at the end."""
|
|
145
|
+
out = []
|
|
146
|
+
found = False
|
|
147
|
+
prefix = f"{key}="
|
|
148
|
+
for line in lines:
|
|
149
|
+
if line.strip().startswith(prefix) or line.startswith(prefix):
|
|
150
|
+
out.append(f"{key}={value}")
|
|
151
|
+
found = True
|
|
152
|
+
else:
|
|
153
|
+
out.append(line)
|
|
154
|
+
if not found:
|
|
155
|
+
out.append(f"{key}={value}")
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main():
|
|
160
|
+
parser = argparse.ArgumentParser()
|
|
161
|
+
parser.add_argument("--quiet", action="store_true")
|
|
162
|
+
parser.add_argument("--force", action="store_true",
|
|
163
|
+
help="Refresh every token regardless of expiry buffer")
|
|
164
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
165
|
+
help="Print what would be refreshed but don't call the API")
|
|
166
|
+
parser.add_argument("--account", default=None,
|
|
167
|
+
help="Only refresh this account (default: all accounts)")
|
|
168
|
+
args = parser.parse_args()
|
|
169
|
+
|
|
170
|
+
def log(msg: str):
|
|
171
|
+
if not args.quiet:
|
|
172
|
+
print(msg)
|
|
173
|
+
|
|
174
|
+
if not IG_ENV_PATH.exists():
|
|
175
|
+
print(f"[refresh-ig-tokens] env file missing: {IG_ENV_PATH}")
|
|
176
|
+
print("SUMMARY:REFRESHED=0 SKIPPED=0 FAILED=0 ACCOUNTS=0")
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
cfg = json.loads(SA_CONFIG.read_text())
|
|
181
|
+
except FileNotFoundError:
|
|
182
|
+
cfg = {}
|
|
183
|
+
accounts_cfg = ((cfg.get("instagram") or {}).get("accounts") or [])
|
|
184
|
+
|
|
185
|
+
if args.account:
|
|
186
|
+
accounts_cfg = [a for a in accounts_cfg
|
|
187
|
+
if a.get("username", "").lower() == args.account.lower()]
|
|
188
|
+
if not accounts_cfg:
|
|
189
|
+
print("[refresh-ig-tokens] no instagram accounts in config")
|
|
190
|
+
print("SUMMARY:REFRESHED=0 SKIPPED=0 FAILED=0 ACCOUNTS=0")
|
|
191
|
+
sys.exit(0)
|
|
192
|
+
|
|
193
|
+
lines = load_env_lines()
|
|
194
|
+
env = env_dict_from_lines(lines)
|
|
195
|
+
now = datetime.now(timezone.utc)
|
|
196
|
+
buffer_secs = REFRESH_BUFFER_DAYS * 86400
|
|
197
|
+
|
|
198
|
+
refreshed = 0
|
|
199
|
+
skipped = 0
|
|
200
|
+
failed = 0
|
|
201
|
+
|
|
202
|
+
for account_cfg in accounts_cfg:
|
|
203
|
+
username = account_cfg.get("username", "")
|
|
204
|
+
token_key = account_cfg.get("ig_long_token_env", "IG_LONG_TOKEN")
|
|
205
|
+
exp_key = expires_key_for(token_key)
|
|
206
|
+
if not exp_key:
|
|
207
|
+
log(f"[refresh-ig-tokens] {username}: cannot derive expires key from {token_key}; skipping")
|
|
208
|
+
skipped += 1
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
cur_token = env.get(token_key)
|
|
212
|
+
if not cur_token:
|
|
213
|
+
log(f"[refresh-ig-tokens] {username}: no value for {token_key}; skipping")
|
|
214
|
+
skipped += 1
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
cur_exp_raw = env.get(exp_key)
|
|
218
|
+
cur_exp = parse_expires(cur_exp_raw)
|
|
219
|
+
if cur_exp is None and not args.force:
|
|
220
|
+
log(f"[refresh-ig-tokens] {username}: {exp_key} unparseable ({cur_exp_raw!r}); skipping (use --force to refresh anyway)")
|
|
221
|
+
skipped += 1
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
if cur_exp is not None and not args.force:
|
|
225
|
+
remaining = (cur_exp - now).total_seconds()
|
|
226
|
+
if remaining > buffer_secs:
|
|
227
|
+
days_left = remaining / 86400
|
|
228
|
+
log(f"[refresh-ig-tokens] {username}: {days_left:.1f}d remaining (> {REFRESH_BUFFER_DAYS}d buffer); skipping")
|
|
229
|
+
skipped += 1
|
|
230
|
+
continue
|
|
231
|
+
if remaining < 0:
|
|
232
|
+
log(f"[refresh-ig-tokens] {username}: EXPIRED {(-remaining)/86400:.1f}d ago; attempting refresh anyway (Meta may reject)")
|
|
233
|
+
|
|
234
|
+
if args.dry_run:
|
|
235
|
+
log(f"[refresh-ig-tokens] {username}: DRY-RUN would refresh {token_key} (exp {cur_exp_raw})")
|
|
236
|
+
refreshed += 1
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
log(f"[refresh-ig-tokens] {username}: refreshing {token_key} (current exp {cur_exp_raw})")
|
|
240
|
+
try:
|
|
241
|
+
resp = refresh_token(cur_token)
|
|
242
|
+
except RefreshError as e:
|
|
243
|
+
log(f"[refresh-ig-tokens] {username}: REFRESH FAILED: {e}")
|
|
244
|
+
failed += 1
|
|
245
|
+
continue
|
|
246
|
+
except Exception as e:
|
|
247
|
+
log(f"[refresh-ig-tokens] {username}: REFRESH FAILED (unexpected): {e}")
|
|
248
|
+
failed += 1
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
new_token = resp.get("access_token")
|
|
252
|
+
expires_in = resp.get("expires_in")
|
|
253
|
+
if not new_token or not expires_in:
|
|
254
|
+
log(f"[refresh-ig-tokens] {username}: refresh response missing fields: {resp}")
|
|
255
|
+
failed += 1
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
new_exp_dt = datetime.now(timezone.utc).fromtimestamp(time.time() + expires_in, tz=timezone.utc)
|
|
259
|
+
new_exp_str = format_expires(new_exp_dt)
|
|
260
|
+
|
|
261
|
+
lines = update_line(lines, token_key, new_token)
|
|
262
|
+
lines = update_line(lines, exp_key, new_exp_str)
|
|
263
|
+
env[token_key] = new_token
|
|
264
|
+
env[exp_key] = new_exp_str
|
|
265
|
+
|
|
266
|
+
log(f"[refresh-ig-tokens] {username}: OK, new expiry {new_exp_str} (~{expires_in/86400:.0f}d)")
|
|
267
|
+
refreshed += 1
|
|
268
|
+
|
|
269
|
+
if refreshed and not args.dry_run:
|
|
270
|
+
write_env_atomic(lines)
|
|
271
|
+
log(f"[refresh-ig-tokens] wrote {IG_ENV_PATH}")
|
|
272
|
+
|
|
273
|
+
print(
|
|
274
|
+
f"SUMMARY:REFRESHED={refreshed} SKIPPED={skipped} FAILED={failed} "
|
|
275
|
+
f"ACCOUNTS={len(accounts_cfg)}"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
main()
|