social-autoposter 1.3.8 → 1.3.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/bin/server.js +84 -12
- package/package.json +1 -1
- package/scripts/reddit_browser.py +152 -32
package/bin/server.js
CHANGED
|
@@ -2454,6 +2454,12 @@ const bookingsPerDayCache = new Map();
|
|
|
2454
2454
|
// post_link_clicks (per-hit log) filtered to is_bot=false so we count humans
|
|
2455
2455
|
// only; joins to post_links + posts so platform/project filters apply.
|
|
2456
2456
|
const clicksPerDayCache = new Map();
|
|
2457
|
+
// Posts-per-day: cached by days|platform|project. Backs the Trends-tab
|
|
2458
|
+
// "Views / Post" ratio denominator. Counts posts.posted_at grouped by UTC date,
|
|
2459
|
+
// excluding moltbook/github/github_issues to match the views/upvotes/comments
|
|
2460
|
+
// scope (those platforms don't surface views, so including them would
|
|
2461
|
+
// understate views-per-post for the comparable platforms).
|
|
2462
|
+
const postsPerDayCache = new Map();
|
|
2457
2463
|
// Funnel-per-day (PostHog-backed metrics): cached by days.
|
|
2458
2464
|
const funnelPerDayCache = new Map();
|
|
2459
2465
|
// Cost-per-day: Claude API session cost attributed to days an activity row
|
|
@@ -4317,6 +4323,56 @@ async function handleApi(req, res) {
|
|
|
4317
4323
|
})().catch(e => json(res, { error: e.message }, 500));
|
|
4318
4324
|
}
|
|
4319
4325
|
|
|
4326
|
+
// GET /api/posts/per-day?days=N&platform=X&project=Y - count of posts we
|
|
4327
|
+
// made per day, sourced from posts.posted_at. Same platform/project filter
|
|
4328
|
+
// shape as views/upvotes/comments so Trends-tab filters behave identically.
|
|
4329
|
+
// Excludes moltbook/github/github_issues to match the views denominator
|
|
4330
|
+
// scope (those platforms don't surface views, so including their posts
|
|
4331
|
+
// would understate the views-per-post ratio). Backs the Trends-tab
|
|
4332
|
+
// "Views / Post" pill.
|
|
4333
|
+
if (p === '/api/posts/per-day' && req.method === 'GET') {
|
|
4334
|
+
if (!req.user.admin) return json(res, { error: 'forbidden' }, 403);
|
|
4335
|
+
const url = new URL(req.url, 'http://localhost');
|
|
4336
|
+
const days = Math.max(1, Math.min(365, parseInt(url.searchParams.get('days') || '30', 10) || 30));
|
|
4337
|
+
const rawPlatform = (url.searchParams.get('platform') || '').trim().toLowerCase();
|
|
4338
|
+
const platform = (rawPlatform === '' || rawPlatform === 'all') ? '' :
|
|
4339
|
+
(rawPlatform === 'x' ? 'twitter' : rawPlatform);
|
|
4340
|
+
const platformOk = platform === '' || /^[a-z0-9_]{1,32}$/.test(platform);
|
|
4341
|
+
if (!platformOk) return json(res, { error: 'invalid platform' }, 400);
|
|
4342
|
+
const rawProject = (url.searchParams.get('project') || '').trim();
|
|
4343
|
+
const project = (rawProject === '' || rawProject.toLowerCase() === 'all') ? '' : rawProject;
|
|
4344
|
+
const projectOk = project === '' || /^[A-Za-z0-9_\-]{1,64}$/.test(project);
|
|
4345
|
+
if (!projectOk) return json(res, { error: 'invalid project' }, 400);
|
|
4346
|
+
const cacheKey = days + '|' + platform + '|' + project;
|
|
4347
|
+
const cached = postsPerDayCache.get(cacheKey);
|
|
4348
|
+
if (cached && Date.now() - cached.at < 300000) {
|
|
4349
|
+
return json(res, { days, rows: cached.value, cachedAt: cached.at });
|
|
4350
|
+
}
|
|
4351
|
+
const platformFilter = platform
|
|
4352
|
+
? " AND CASE WHEN LOWER(p.platform) = 'x' THEN 'twitter' ELSE LOWER(p.platform) END = '" + platform + "'"
|
|
4353
|
+
: '';
|
|
4354
|
+
const projectFilter = project
|
|
4355
|
+
? " AND p.project_name = '" + project.replace(/'/g, "''") + "'"
|
|
4356
|
+
: '';
|
|
4357
|
+
const q =
|
|
4358
|
+
"SELECT json_agg(row_to_json(r)) FROM (" +
|
|
4359
|
+
"SELECT to_char((p.posted_at AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD') AS day, " +
|
|
4360
|
+
"COUNT(*)::bigint AS posts_made " +
|
|
4361
|
+
"FROM posts p " +
|
|
4362
|
+
"WHERE p.posted_at IS NOT NULL " +
|
|
4363
|
+
"AND p.posted_at >= CURRENT_DATE - INTERVAL '" + days + " days' " +
|
|
4364
|
+
"AND LOWER(p.platform) NOT IN ('moltbook', 'github', 'github_issues')" +
|
|
4365
|
+
platformFilter + projectFilter + " " +
|
|
4366
|
+
"GROUP BY day ORDER BY day ASC" +
|
|
4367
|
+
") r";
|
|
4368
|
+
return (async () => {
|
|
4369
|
+
const rows = await pq(q);
|
|
4370
|
+
const value = (rows && rows.length && rows[0].json_agg) ? rows[0].json_agg : [];
|
|
4371
|
+
postsPerDayCache.set(cacheKey, { at: Date.now(), value });
|
|
4372
|
+
return json(res, { days, rows: value });
|
|
4373
|
+
})().catch(e => json(res, { error: e.message }, 500));
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4320
4376
|
// GET /api/bookings/per-day?days=N - real Cal.com bookings per day from
|
|
4321
4377
|
// the separate BOOKINGS_DATABASE_URL Neon DB. Filters out test bookings
|
|
4322
4378
|
// the same way project_stats_json.py does (attendee_email NOT ILIKE
|
|
@@ -9989,6 +10045,11 @@ function renderDailyMetrics() {
|
|
|
9989
10045
|
// no new fetch needed. Values are percentages (0-100), formatted to one
|
|
9990
10046
|
// decimal place; days with views=0 are dropped (ratios are undefined).
|
|
9991
10047
|
let RATIO_METRICS = [
|
|
10048
|
+
// Views per post: how many views a post earns on average per day in the
|
|
10049
|
+
// window. Numerator is views_gained that day; denominator is posts_made
|
|
10050
|
+
// that day. format='count' renders as plain K/M numbers (e.g. "1.2K")
|
|
10051
|
+
// since the value is a count, not a percentage or dollar figure.
|
|
10052
|
+
{ id: 'views_per_post', label: 'Views / Post', color: '#a855f7', numerator: 'views', denominator: 'posts', format: 'count', scaleFactor: 1 },
|
|
9992
10053
|
{ id: 'upvotes_per_view', label: 'Upvotes / Views', color: '#f97316', numerator: 'upvotes', denominator: 'views', format: 'pct', scaleFactor: 100 },
|
|
9993
10054
|
{ id: 'comments_per_view', label: 'Comments / Views', color: '#14b8a6', numerator: 'comments', denominator: 'views', format: 'pct', scaleFactor: 100 },
|
|
9994
10055
|
{ id: 'clicks_per_view', label: 'Clicks / Views', color: '#0ea5e9', numerator: 'clicks', denominator: 'views', format: 'pct', scaleFactor: 100 },
|
|
@@ -10007,11 +10068,11 @@ let RATIO_METRICS = [
|
|
|
10007
10068
|
{ id: 'cost_per_kviews', label: 'Cost / 1k Views', color: '#dc2626', numerator: 'cost', denominator: 'views', format: 'usd', scaleFactor: 1000, adminOnly: true },
|
|
10008
10069
|
{ id: 'cost_per_kvisitors', label: 'Cost / 1k Visitors', color: '#7c3aed', numerator: 'cost', denominator: 'pageviews', format: 'usd', scaleFactor: 1000, adminOnly: true },
|
|
10009
10070
|
];
|
|
10010
|
-
const RATIO_METRICS_DEFAULTS = ['upvotes_per_view', 'comments_per_view', 'clicks_per_view', 'email_signups_per_session', 'schedule_clicks_per_session', 'get_started_per_session', 'cost_per_kviews', 'cost_per_kvisitors'];
|
|
10011
|
-
// .
|
|
10012
|
-
//
|
|
10013
|
-
//
|
|
10014
|
-
const RATIO_METRICS_STORAGE_KEY = 'ratioMetricsActive.
|
|
10071
|
+
const RATIO_METRICS_DEFAULTS = ['views_per_post', 'upvotes_per_view', 'comments_per_view', 'clicks_per_view', 'email_signups_per_session', 'schedule_clicks_per_session', 'get_started_per_session', 'cost_per_kviews', 'cost_per_kvisitors'];
|
|
10072
|
+
// .v3: ratio set expanded to include views_per_post at the head. Bumping
|
|
10073
|
+
// the storage key seeds the new defaults exactly once so existing users
|
|
10074
|
+
// see the new ratio pre-selected the next time they open Trends.
|
|
10075
|
+
const RATIO_METRICS_STORAGE_KEY = 'ratioMetricsActive.v3';
|
|
10015
10076
|
let _ratioMetricsActive = null;
|
|
10016
10077
|
function _loadRatioMetricsActive() {
|
|
10017
10078
|
if (_ratioMetricsActive) return _ratioMetricsActive;
|
|
@@ -10036,6 +10097,12 @@ function _fmtPct(n) {
|
|
|
10036
10097
|
function _fmtForRatio(r, n) {
|
|
10037
10098
|
if (n == null || !isFinite(n)) return '—';
|
|
10038
10099
|
if (r && r.format === 'usd') return _fmtUsd(n);
|
|
10100
|
+
if (r && r.format === 'count') {
|
|
10101
|
+
// Whole-number K/M counts (no decimals) for ratios like Views / Post.
|
|
10102
|
+
if (n >= 1_000_000) return Math.round(n / 1_000_000) + 'M';
|
|
10103
|
+
if (n >= 1_000) return Math.round(n / 1_000) + 'K';
|
|
10104
|
+
return String(Math.round(n));
|
|
10105
|
+
}
|
|
10039
10106
|
return _fmtPct(n);
|
|
10040
10107
|
}
|
|
10041
10108
|
|
|
@@ -10203,10 +10270,13 @@ function renderRatioMetrics() {
|
|
|
10203
10270
|
const day = days[idx];
|
|
10204
10271
|
const rows = visible.map(r => {
|
|
10205
10272
|
const v = ratioSeries[r.id][day];
|
|
10206
|
-
// "no views" / "no visitors" depending on the
|
|
10207
|
-
// ratios use the right unit so the empty-day
|
|
10208
|
-
// ratio's meaning ("no views" for
|
|
10209
|
-
|
|
10273
|
+
// "no views" / "no visitors" / "no posts" depending on the
|
|
10274
|
+
// denominator. Cost ratios use the right unit so the empty-day
|
|
10275
|
+
// message matches the ratio's meaning ("no views" for
|
|
10276
|
+
// cost_per_kviews, etc.).
|
|
10277
|
+
const emptyLabel = (r.denominator === 'pageviews') ? 'no visitors'
|
|
10278
|
+
: (r.denominator === 'posts') ? 'no posts'
|
|
10279
|
+
: 'no views';
|
|
10210
10280
|
const display = (v == null || !isFinite(v)) ? emptyLabel : _fmtForRatio(r, v);
|
|
10211
10281
|
return '<div class="tt-row"><span class="swatch" style="background:' + r.color + ';"></span>' +
|
|
10212
10282
|
'<span>' + escapeHtml(r.label) + '</span>' +
|
|
@@ -10355,7 +10425,7 @@ async function loadDailyMetrics() {
|
|
|
10355
10425
|
const qsProj = projectOnlyParams.join('&');
|
|
10356
10426
|
try {
|
|
10357
10427
|
const costAvail = window.SA_IS_ADMIN !== false;
|
|
10358
|
-
const [views, upvotes, comments, clicks, bookings, funnel, cost] = await Promise.all([
|
|
10428
|
+
const [views, upvotes, comments, clicks, bookings, funnel, cost, posts] = await Promise.all([
|
|
10359
10429
|
fetchOne('/api/views/per-day?' + qsAware),
|
|
10360
10430
|
fetchOne('/api/upvotes/per-day?' + qsAware),
|
|
10361
10431
|
fetchOne('/api/comments/per-day?' + qsAware),
|
|
@@ -10363,8 +10433,9 @@ async function loadDailyMetrics() {
|
|
|
10363
10433
|
fetchOne('/api/bookings/per-day?' + qsProj),
|
|
10364
10434
|
fetchOne('/api/funnel/per-day?' + qsProj),
|
|
10365
10435
|
costAvail ? fetchOne('/api/cost/per-day?' + qsAware) : Promise.resolve({ rows: [], failed: false }),
|
|
10436
|
+
fetchOne('/api/posts/per-day?' + qsAware),
|
|
10366
10437
|
]);
|
|
10367
|
-
const allFailed = [views, upvotes, comments, clicks, bookings, funnel, cost].every(r => r.failed);
|
|
10438
|
+
const allFailed = [views, upvotes, comments, clicks, bookings, funnel, cost, posts].every(r => r.failed);
|
|
10368
10439
|
if (allFailed) {
|
|
10369
10440
|
if (chartEl) chartEl.innerHTML = '<div class="views-chart-empty">Unable to load daily metrics (all endpoints failed).</div>';
|
|
10370
10441
|
return;
|
|
@@ -10380,6 +10451,7 @@ async function loadDailyMetrics() {
|
|
|
10380
10451
|
intoSeries('clicks', clicks.rows, 'clicks_gained');
|
|
10381
10452
|
intoSeries('bookings', bookings.rows, 'bookings_gained');
|
|
10382
10453
|
intoSeries('cost', cost.rows, 'cost_usd');
|
|
10454
|
+
intoSeries('posts', posts.rows, 'posts_made');
|
|
10383
10455
|
DAILY_METRICS.filter(m => m.funnel).forEach(m => {
|
|
10384
10456
|
intoSeries(m.id, funnel.rows, m.valueKey);
|
|
10385
10457
|
});
|
|
@@ -10402,7 +10474,7 @@ async function loadDailyMetrics() {
|
|
|
10402
10474
|
// Stash a list of failed endpoints so renderDailyMetrics can surface a
|
|
10403
10475
|
// small "(N timed out)" hint in the status pill rather than silently
|
|
10404
10476
|
// showing flat zeros for those series.
|
|
10405
|
-
const fetchResults = { views, upvotes, comments, clicks, bookings, funnel, cost };
|
|
10477
|
+
const fetchResults = { views, upvotes, comments, clicks, bookings, funnel, cost, posts };
|
|
10406
10478
|
_dailyMetricsFailed = Object.keys(fetchResults)
|
|
10407
10479
|
.filter(k => fetchResults[k].failed)
|
|
10408
10480
|
.map(k => ({ key: k, timedOut: !!fetchResults[k].timedOut }));
|
package/package.json
CHANGED
|
@@ -61,14 +61,23 @@ def _diag_log(msg):
|
|
|
61
61
|
pass
|
|
62
62
|
VIEWPORT = {"width": 911, "height": 1016}
|
|
63
63
|
|
|
64
|
-
# Load Reddit username from config
|
|
64
|
+
# Load Reddit username from config.
|
|
65
|
+
# Prefers the new top-level `reddit_account.username` (2026-05-15) over the
|
|
66
|
+
# legacy `accounts.reddit.username` path. Drift between the two silently
|
|
67
|
+
# broke the post-permalink lookup on the VM (wrong username → JS finds 0
|
|
68
|
+
# matching comments → permalink=None → pipeline records `failed` despite
|
|
69
|
+
# the comment landing on Reddit).
|
|
65
70
|
_config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")
|
|
66
71
|
OUR_USERNAME = "Deep_Ad1959"
|
|
67
72
|
if os.path.exists(_config_path):
|
|
68
73
|
try:
|
|
69
74
|
with open(_config_path) as f:
|
|
70
75
|
_cfg = json.load(f)
|
|
71
|
-
OUR_USERNAME =
|
|
76
|
+
OUR_USERNAME = (
|
|
77
|
+
(_cfg.get("reddit_account") or {}).get("username")
|
|
78
|
+
or _cfg.get("accounts", {}).get("reddit", {}).get("username")
|
|
79
|
+
or OUR_USERNAME
|
|
80
|
+
)
|
|
72
81
|
except Exception:
|
|
73
82
|
pass
|
|
74
83
|
|
|
@@ -295,22 +304,52 @@ def _refresh_browser_lock():
|
|
|
295
304
|
|
|
296
305
|
|
|
297
306
|
def get_browser_and_page(playwright):
|
|
298
|
-
"""
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
+
"""Get a logged-in Reddit page, preferring CDP-attach over launch_persistent_context.
|
|
308
|
+
|
|
309
|
+
Two paths:
|
|
310
|
+
1. CDP-attach (preferred on appmaker/e2b VM and any host running a visible
|
|
311
|
+
logged-in Chromium): connect to the existing browser, find a context with
|
|
312
|
+
a live reddit_session cookie, open a NEW PAGE on that context.
|
|
313
|
+
2. launch_persistent_context fallback: when CDP isn't available OR contexts
|
|
314
|
+
have no reddit_session (laptop where reddit-agent MCP isolates its session
|
|
315
|
+
in an invisible context).
|
|
316
|
+
|
|
317
|
+
Why CDP-attach matters: appmaker's visible Chromium permanently holds
|
|
318
|
+
/root/.chromium-profile. launch_persistent_context collides on profile leveldb
|
|
319
|
+
locks, loads a partial session, and EVERY post returns account_blocked_in_sub
|
|
320
|
+
because the comment form never renders. Attaching to the live context dodges
|
|
321
|
+
the collision entirely.
|
|
322
|
+
|
|
323
|
+
Returns (browser, page, is_cdp). When is_cdp=True, callers must close ONLY
|
|
324
|
+
the page (not page.context) and NOT the browser; closing context[0] or the
|
|
325
|
+
CDP browser would kill the user's visible session.
|
|
307
326
|
"""
|
|
308
327
|
_acquire_browser_lock()
|
|
309
328
|
cdp_port = find_reddit_cdp_port()
|
|
310
329
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
330
|
+
if cdp_port:
|
|
331
|
+
try:
|
|
332
|
+
cdp_browser = playwright.chromium.connect_over_cdp(f"http://localhost:{cdp_port}")
|
|
333
|
+
for ctx in cdp_browser.contexts:
|
|
334
|
+
try:
|
|
335
|
+
cookies = ctx.cookies("https://www.reddit.com/")
|
|
336
|
+
except Exception:
|
|
337
|
+
cookies = []
|
|
338
|
+
has_session = any(
|
|
339
|
+
c.get("name") == "reddit_session" and c.get("value")
|
|
340
|
+
for c in cookies
|
|
341
|
+
)
|
|
342
|
+
if has_session:
|
|
343
|
+
page = ctx.new_page()
|
|
344
|
+
return cdp_browser, page, True
|
|
345
|
+
try:
|
|
346
|
+
cdp_browser.close()
|
|
347
|
+
except Exception:
|
|
348
|
+
pass
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
# Fallback: launch our own persistent context against PROFILE_DIR.
|
|
314
353
|
# Retry on Chromium SingletonLock collisions (MCP holds the OS-level profile
|
|
315
354
|
# lock for its entire server lifetime; the JSON lock can expire while the
|
|
316
355
|
# OS lock is still held).
|
|
@@ -496,9 +535,18 @@ def post_comment(thread_url, text):
|
|
|
496
535
|
}
|
|
497
536
|
|
|
498
537
|
finally:
|
|
499
|
-
|
|
538
|
+
try:
|
|
539
|
+
if is_cdp:
|
|
540
|
+
page.close()
|
|
541
|
+
else:
|
|
542
|
+
page.context.close()
|
|
543
|
+
except Exception:
|
|
544
|
+
pass
|
|
500
545
|
if not is_cdp:
|
|
501
|
-
|
|
546
|
+
try:
|
|
547
|
+
browser.close()
|
|
548
|
+
except Exception:
|
|
549
|
+
pass
|
|
502
550
|
|
|
503
551
|
|
|
504
552
|
def reply_to_comment(comment_permalink, text, dm_id=None):
|
|
@@ -736,9 +784,18 @@ def reply_to_comment(comment_permalink, text, dm_id=None):
|
|
|
736
784
|
}
|
|
737
785
|
|
|
738
786
|
finally:
|
|
739
|
-
|
|
787
|
+
try:
|
|
788
|
+
if is_cdp:
|
|
789
|
+
page.close()
|
|
790
|
+
else:
|
|
791
|
+
page.context.close()
|
|
792
|
+
except Exception:
|
|
793
|
+
pass
|
|
740
794
|
if not is_cdp:
|
|
741
|
-
|
|
795
|
+
try:
|
|
796
|
+
browser.close()
|
|
797
|
+
except Exception:
|
|
798
|
+
pass
|
|
742
799
|
|
|
743
800
|
|
|
744
801
|
def edit_comment(comment_permalink, new_text):
|
|
@@ -859,9 +916,18 @@ def edit_comment(comment_permalink, new_text):
|
|
|
859
916
|
}
|
|
860
917
|
|
|
861
918
|
finally:
|
|
862
|
-
|
|
919
|
+
try:
|
|
920
|
+
if is_cdp:
|
|
921
|
+
page.close()
|
|
922
|
+
else:
|
|
923
|
+
page.context.close()
|
|
924
|
+
except Exception:
|
|
925
|
+
pass
|
|
863
926
|
if not is_cdp:
|
|
864
|
-
|
|
927
|
+
try:
|
|
928
|
+
browser.close()
|
|
929
|
+
except Exception:
|
|
930
|
+
pass
|
|
865
931
|
|
|
866
932
|
|
|
867
933
|
def edit_thread(thread_permalink, new_body):
|
|
@@ -956,9 +1022,18 @@ def edit_thread(thread_permalink, new_body):
|
|
|
956
1022
|
}
|
|
957
1023
|
|
|
958
1024
|
finally:
|
|
959
|
-
|
|
1025
|
+
try:
|
|
1026
|
+
if is_cdp:
|
|
1027
|
+
page.close()
|
|
1028
|
+
else:
|
|
1029
|
+
page.context.close()
|
|
1030
|
+
except Exception:
|
|
1031
|
+
pass
|
|
960
1032
|
if not is_cdp:
|
|
961
|
-
|
|
1033
|
+
try:
|
|
1034
|
+
browser.close()
|
|
1035
|
+
except Exception:
|
|
1036
|
+
pass
|
|
962
1037
|
|
|
963
1038
|
|
|
964
1039
|
def unread_dms():
|
|
@@ -1135,9 +1210,18 @@ def unread_dms():
|
|
|
1135
1210
|
return unique
|
|
1136
1211
|
|
|
1137
1212
|
finally:
|
|
1138
|
-
|
|
1213
|
+
try:
|
|
1214
|
+
if is_cdp:
|
|
1215
|
+
page.close()
|
|
1216
|
+
else:
|
|
1217
|
+
page.context.close()
|
|
1218
|
+
except Exception:
|
|
1219
|
+
pass
|
|
1139
1220
|
if not is_cdp:
|
|
1140
|
-
|
|
1221
|
+
try:
|
|
1222
|
+
browser.close()
|
|
1223
|
+
except Exception:
|
|
1224
|
+
pass
|
|
1141
1225
|
|
|
1142
1226
|
|
|
1143
1227
|
def read_conversation(chat_url, max_messages=20):
|
|
@@ -1293,9 +1377,18 @@ def read_conversation(chat_url, max_messages=20):
|
|
|
1293
1377
|
return result
|
|
1294
1378
|
|
|
1295
1379
|
finally:
|
|
1296
|
-
|
|
1380
|
+
try:
|
|
1381
|
+
if is_cdp:
|
|
1382
|
+
page.close()
|
|
1383
|
+
else:
|
|
1384
|
+
page.context.close()
|
|
1385
|
+
except Exception:
|
|
1386
|
+
pass
|
|
1297
1387
|
if not is_cdp:
|
|
1298
|
-
|
|
1388
|
+
try:
|
|
1389
|
+
browser.close()
|
|
1390
|
+
except Exception:
|
|
1391
|
+
pass
|
|
1299
1392
|
|
|
1300
1393
|
|
|
1301
1394
|
def _load_active_reddit_campaigns_for_dm():
|
|
@@ -1564,9 +1657,18 @@ def send_dm(chat_url, message, dm_id=None):
|
|
|
1564
1657
|
}
|
|
1565
1658
|
|
|
1566
1659
|
finally:
|
|
1567
|
-
|
|
1660
|
+
try:
|
|
1661
|
+
if is_cdp:
|
|
1662
|
+
page.close()
|
|
1663
|
+
else:
|
|
1664
|
+
page.context.close()
|
|
1665
|
+
except Exception:
|
|
1666
|
+
pass
|
|
1568
1667
|
if not is_cdp:
|
|
1569
|
-
|
|
1668
|
+
try:
|
|
1669
|
+
browser.close()
|
|
1670
|
+
except Exception:
|
|
1671
|
+
pass
|
|
1570
1672
|
|
|
1571
1673
|
|
|
1572
1674
|
def compose_dm(recipient, subject, body):
|
|
@@ -1859,9 +1961,18 @@ def compose_dm(recipient, subject, body):
|
|
|
1859
1961
|
return {"ok": True, "thread_url": page.url}
|
|
1860
1962
|
|
|
1861
1963
|
finally:
|
|
1862
|
-
|
|
1964
|
+
try:
|
|
1965
|
+
if is_cdp:
|
|
1966
|
+
page.close()
|
|
1967
|
+
else:
|
|
1968
|
+
page.context.close()
|
|
1969
|
+
except Exception:
|
|
1970
|
+
pass
|
|
1863
1971
|
if not is_cdp:
|
|
1864
|
-
|
|
1972
|
+
try:
|
|
1973
|
+
browser.close()
|
|
1974
|
+
except Exception:
|
|
1975
|
+
pass
|
|
1865
1976
|
|
|
1866
1977
|
|
|
1867
1978
|
def scrape_views(username, max_scrolls=300):
|
|
@@ -2022,9 +2133,18 @@ def scrape_views(username, max_scrolls=300):
|
|
|
2022
2133
|
except Exception as e:
|
|
2023
2134
|
return {"ok": False, "error": str(e)}
|
|
2024
2135
|
finally:
|
|
2025
|
-
|
|
2136
|
+
try:
|
|
2137
|
+
if is_cdp:
|
|
2138
|
+
page.close()
|
|
2139
|
+
else:
|
|
2140
|
+
page.context.close()
|
|
2141
|
+
except Exception:
|
|
2142
|
+
pass
|
|
2026
2143
|
if not is_cdp:
|
|
2027
|
-
|
|
2144
|
+
try:
|
|
2145
|
+
browser.close()
|
|
2146
|
+
except Exception:
|
|
2147
|
+
pass
|
|
2028
2148
|
|
|
2029
2149
|
|
|
2030
2150
|
def main():
|