@gonzih/of-scraper 1.0.0

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.
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scrapeThreadMessages = exports.scrapeNewMessages = void 0;
4
+ async function scrapeNewMessages(page) {
5
+ const ts = new Date().toISOString();
6
+ console.log(`[${ts}] Polling https://onlyfans.com/my/chats for new messages`);
7
+ try {
8
+ await page.goto('https://onlyfans.com/my/chats', {
9
+ waitUntil: 'networkidle2',
10
+ timeout: 30000,
11
+ });
12
+ }
13
+ catch (err) {
14
+ console.error(`[${new Date().toISOString()}] Failed to navigate to chats:`, err);
15
+ return [];
16
+ }
17
+ try {
18
+ await page.waitForSelector('.b-chats__list, .b-chat-item, [class*="chat-list"], [class*="chatItem"]', {
19
+ timeout: 15000,
20
+ });
21
+ }
22
+ catch {
23
+ console.warn(`[${new Date().toISOString()}] Chat list selector not found — page may not be loaded`);
24
+ return [];
25
+ }
26
+ const messages = await page.evaluate(() => {
27
+ const results = [];
28
+ const chatItemSelectors = [
29
+ '.b-chat-item',
30
+ '[class*="chatItem"]',
31
+ '[data-type="chat-item"]',
32
+ '.b-chats__item',
33
+ ];
34
+ let chatItems = [];
35
+ for (const sel of chatItemSelectors) {
36
+ const found = Array.from(document.querySelectorAll(sel));
37
+ if (found.length > 0) {
38
+ chatItems = found;
39
+ break;
40
+ }
41
+ }
42
+ for (const item of chatItems) {
43
+ try {
44
+ const link = item.querySelector('a[href*="/my/chats/chat/"]');
45
+ const href = link?.getAttribute('href') ?? '';
46
+ const threadMatch = href.match(/\/my\/chats\/chat\/(\d+)/);
47
+ const threadId = threadMatch ? threadMatch[1] : '';
48
+ const nameEl = item.querySelector('.b-chat-item__name') ??
49
+ item.querySelector('[class*="userName"]') ??
50
+ item.querySelector('[class*="name"]');
51
+ const fromUsername = nameEl?.textContent?.trim() ?? '';
52
+ const textEl = item.querySelector('.b-chat-item__message') ??
53
+ item.querySelector('[class*="lastMessage"]') ??
54
+ item.querySelector('[class*="message"]');
55
+ const text = textEl?.textContent?.trim() ?? '';
56
+ const timeEl = item.querySelector('time') ??
57
+ item.querySelector('[class*="time"]') ??
58
+ item.querySelector('[class*="date"]');
59
+ const timestamp = timeEl?.getAttribute('datetime') ?? timeEl?.textContent?.trim() ?? new Date().toISOString();
60
+ const messageId = `${threadId}_${btoa(encodeURIComponent(text)).slice(0, 16)}`;
61
+ if (threadId && fromUsername) {
62
+ results.push({ messageId, fromUsername, text, timestamp, threadId });
63
+ }
64
+ }
65
+ catch {
66
+ // Skip malformed items
67
+ }
68
+ }
69
+ return results;
70
+ });
71
+ console.log(`[${new Date().toISOString()}] Found ${messages.length} chat threads`);
72
+ return messages;
73
+ }
74
+ exports.scrapeNewMessages = scrapeNewMessages;
75
+ async function scrapeThreadMessages(page, threadId) {
76
+ const url = `https://onlyfans.com/my/chats/chat/${threadId}`;
77
+ try {
78
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
79
+ }
80
+ catch (err) {
81
+ console.error(`[${new Date().toISOString()}] Failed to navigate to thread ${threadId}:`, err);
82
+ return [];
83
+ }
84
+ try {
85
+ await page.waitForSelector('.b-chat__messages, [class*="messages"]', { timeout: 15000 });
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ const messages = await page.evaluate((tId) => {
91
+ const results = [];
92
+ const messageSelectors = [
93
+ '.b-message__wrapper',
94
+ '[class*="messageItem"]',
95
+ '[class*="message-item"]',
96
+ ];
97
+ let items = [];
98
+ for (const sel of messageSelectors) {
99
+ const found = Array.from(document.querySelectorAll(sel));
100
+ if (found.length > 0) {
101
+ items = found;
102
+ break;
103
+ }
104
+ }
105
+ for (const item of items) {
106
+ try {
107
+ const isSelf = item.classList.contains('m-my') ||
108
+ item.querySelector('[class*="my-message"]') !== null ||
109
+ item.getAttribute('data-type') === 'outgoing';
110
+ if (isSelf)
111
+ continue;
112
+ const textEl = item.querySelector('.b-message__text, [class*="messageText"]');
113
+ const text = textEl?.textContent?.trim() ?? '';
114
+ const timeEl = item.querySelector('time, [class*="time"]');
115
+ const timestamp = timeEl?.getAttribute('datetime') ?? timeEl?.textContent?.trim() ?? new Date().toISOString();
116
+ const messageId = item.getAttribute('data-id') ??
117
+ item.getAttribute('id') ??
118
+ `${tId}_${btoa(encodeURIComponent(timestamp + text)).slice(0, 16)}`;
119
+ const usernameEl = item.querySelector('[class*="userName"], [class*="name"]');
120
+ const fromUsername = usernameEl?.textContent?.trim() ?? '';
121
+ if (text) {
122
+ results.push({ messageId, fromUsername, text, timestamp, threadId: tId });
123
+ }
124
+ }
125
+ catch {
126
+ // skip
127
+ }
128
+ }
129
+ return results;
130
+ }, threadId);
131
+ return messages;
132
+ }
133
+ exports.scrapeThreadMessages = scrapeThreadMessages;
134
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Page } from 'puppeteer';
2
+ export interface Profile {
3
+ username: string;
4
+ displayName: string;
5
+ isSubscribed: boolean;
6
+ profilePic: string;
7
+ bio: string;
8
+ fetchedAt: string;
9
+ }
10
+ export declare function scrapeProfile(page: Page, username: string): Promise<Profile | null>;
11
+ //# sourceMappingURL=profiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CA6FzF"}
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scrapeProfile = void 0;
4
+ async function scrapeProfile(page, username) {
5
+ const url = `https://onlyfans.com/${username}`;
6
+ console.log(`[${new Date().toISOString()}] Fetching profile for @${username}`);
7
+ try {
8
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
9
+ }
10
+ catch (err) {
11
+ console.error(`[${new Date().toISOString()}] Failed to navigate to profile ${username}:`, err);
12
+ return null;
13
+ }
14
+ try {
15
+ await page.waitForSelector('.b-profile__main, [class*="profile"], .b-user-card, [class*="userCard"]', { timeout: 15000 });
16
+ }
17
+ catch {
18
+ console.warn(`[${new Date().toISOString()}] Profile selectors not found for @${username}`);
19
+ }
20
+ const profile = await page.evaluate((uname) => {
21
+ const nameSelectors = [
22
+ '.b-username__name',
23
+ '[class*="userName"]',
24
+ 'h1[class*="name"]',
25
+ '.g-user-name',
26
+ ];
27
+ let displayName = uname;
28
+ for (const sel of nameSelectors) {
29
+ const el = document.querySelector(sel);
30
+ if (el?.textContent?.trim()) {
31
+ displayName = el.textContent.trim();
32
+ break;
33
+ }
34
+ }
35
+ const picSelectors = [
36
+ '.b-user-avatar img',
37
+ '[class*="avatar"] img',
38
+ '.g-avatar img',
39
+ 'img[class*="avatar"]',
40
+ ];
41
+ let profilePic = '';
42
+ for (const sel of picSelectors) {
43
+ const el = document.querySelector(sel);
44
+ if (el?.src) {
45
+ profilePic = el.src;
46
+ break;
47
+ }
48
+ }
49
+ const bioSelectors = [
50
+ '.b-profile__about-text',
51
+ '[class*="bio"]',
52
+ '[class*="about"]',
53
+ '.g-desc',
54
+ ];
55
+ let bio = '';
56
+ for (const sel of bioSelectors) {
57
+ const el = document.querySelector(sel);
58
+ if (el?.textContent?.trim()) {
59
+ bio = el.textContent.trim();
60
+ break;
61
+ }
62
+ }
63
+ const subSelectors = [
64
+ '[class*="subscribeButton"]',
65
+ 'button[class*="subscribe"]',
66
+ '.b-btn-subscibe',
67
+ ];
68
+ let isSubscribed = false;
69
+ for (const sel of subSelectors) {
70
+ const el = document.querySelector(sel);
71
+ if (el) {
72
+ const btnText = el.textContent?.toLowerCase() ?? '';
73
+ isSubscribed = btnText.includes('subscribed') || btnText.includes('unsubscribe');
74
+ break;
75
+ }
76
+ }
77
+ return {
78
+ username: uname,
79
+ displayName,
80
+ isSubscribed,
81
+ profilePic,
82
+ bio,
83
+ fetchedAt: new Date().toISOString(),
84
+ };
85
+ }, username);
86
+ console.log(`[${new Date().toISOString()}] Profile fetched for @${username}: ${profile.displayName}`);
87
+ return profile;
88
+ }
89
+ exports.scrapeProfile = scrapeProfile;
90
+ //# sourceMappingURL=profiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiles.js","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import Redis from 'ioredis';
2
+ export declare const redis: Redis;
3
+ export declare function connectRedis(): Promise<void>;
4
+ export declare function xadd(stream: string, fields: Record<string, string>): Promise<string | null>;
5
+ export declare function isSeen(messageId: string): Promise<boolean>;
6
+ export declare function markSeen(messageId: string): Promise<void>;
7
+ //# sourceMappingURL=redis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../src/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,SAAS,CAAC;AAI5B,eAAO,MAAM,KAAK,OAGhB,CAAC;AAMH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAGlD;AAED,wBAAsB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMjG;AAED,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGhE;AAED,wBAAsB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/D"}
package/dist/redis.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.markSeen = exports.isSeen = exports.xadd = exports.connectRedis = exports.redis = void 0;
7
+ const ioredis_1 = __importDefault(require("ioredis"));
8
+ const REDIS_URL = process.env.REDIS_URL ?? 'redis://localhost:6379';
9
+ exports.redis = new ioredis_1.default(REDIS_URL, {
10
+ lazyConnect: true,
11
+ maxRetriesPerRequest: 3,
12
+ });
13
+ exports.redis.on('error', (err) => {
14
+ console.error(`[${new Date().toISOString()}] Redis error:`, err.message);
15
+ });
16
+ async function connectRedis() {
17
+ await exports.redis.connect();
18
+ console.log(`[${new Date().toISOString()}] Redis connected to ${REDIS_URL}`);
19
+ }
20
+ exports.connectRedis = connectRedis;
21
+ async function xadd(stream, fields) {
22
+ const args = [];
23
+ for (const [k, v] of Object.entries(fields)) {
24
+ args.push(k, v);
25
+ }
26
+ return exports.redis.xadd(stream, '*', ...args);
27
+ }
28
+ exports.xadd = xadd;
29
+ async function isSeen(messageId) {
30
+ const result = await exports.redis.sismember('of:seen_messages', messageId);
31
+ return result === 1;
32
+ }
33
+ exports.isSeen = isSeen;
34
+ async function markSeen(messageId) {
35
+ await exports.redis.sadd('of:seen_messages', messageId);
36
+ }
37
+ exports.markSeen = markSeen;
38
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.js","sourceRoot":"","sources":["../src/redis.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA4B;AAE5B,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,wBAAwB,CAAC;AAEvD,QAAA,KAAK,GAAG,IAAI,iBAAK,CAAC,SAAS,EAAE;IACxC,WAAW,EAAE,IAAI;IACjB,oBAAoB,EAAE,CAAC;CACxB,CAAC,CAAC;AAEH,aAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;IAC/B,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,gBAAgB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;AAC3E,CAAC,CAAC,CAAC;AAEI,KAAK,UAAU,YAAY;IAChC,MAAM,aAAK,CAAC,OAAO,EAAE,CAAC;IACtB,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,wBAAwB,SAAS,EAAE,CAAC,CAAC;AAC/E,CAAC;AAHD,oCAGC;AAEM,KAAK,UAAU,IAAI,CAAC,MAAc,EAAE,MAA8B;IACvE,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,aAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AAC1C,CAAC;AAND,oBAMC;AAEM,KAAK,UAAU,MAAM,CAAC,SAAiB;IAC5C,MAAM,MAAM,GAAG,MAAM,aAAK,CAAC,SAAS,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC;IACpE,OAAO,MAAM,KAAK,CAAC,CAAC;AACtB,CAAC;AAHD,wBAGC;AAEM,KAAK,UAAU,QAAQ,CAAC,SAAiB;IAC9C,MAAM,aAAK,CAAC,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC;AAClD,CAAC;AAFD,4BAEC"}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@gonzih/of-scraper",
3
+ "version": "1.0.0",
4
+ "description": "OnlyFans scraper that feeds messages, profiles, and financials into Redis Streams",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "@gonzih/of-scraper": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "dev": "ts-node src/index.ts"
13
+ },
14
+ "keywords": [],
15
+ "author": "gonzih",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "ioredis": "5.3.2",
19
+ "puppeteer": "25.0.4",
20
+ "puppeteer-extra": "3.3.6",
21
+ "puppeteer-extra-plugin-stealth": "2.11.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "20.11.5",
25
+ "typescript": "5.3.3"
26
+ }
27
+ }
package/src/browser.ts ADDED
@@ -0,0 +1,134 @@
1
+ import puppeteer from 'puppeteer-extra';
2
+ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
3
+ import type { Browser, Page } from 'puppeteer';
4
+ import type { CookieParam } from 'puppeteer-core';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+
8
+ puppeteer.use(StealthPlugin());
9
+
10
+ const SESSION_FILE = process.env.SESSION_FILE ?? './session.json';
11
+ const HEADLESS = process.env.HEADLESS !== 'false';
12
+
13
+ interface SessionData {
14
+ cookies: CookieParam[];
15
+ localStorage: Record<string, string>;
16
+ }
17
+
18
+ export async function launchBrowser(): Promise<Browser> {
19
+ const browser = await puppeteer.launch({
20
+ headless: HEADLESS,
21
+ args: [
22
+ '--no-sandbox',
23
+ '--disable-setuid-sandbox',
24
+ '--disable-blink-features=AutomationControlled',
25
+ ],
26
+ });
27
+ console.log(`[${new Date().toISOString()}] Browser launched (headless=${HEADLESS})`);
28
+ return browser;
29
+ }
30
+
31
+ export async function saveSession(page: Page): Promise<void> {
32
+ const cookies = await page.cookies();
33
+ const localStorage: Record<string, string> = await page.evaluate(() => {
34
+ const data: Record<string, string> = {};
35
+ for (let i = 0; i < window.localStorage.length; i++) {
36
+ const key = window.localStorage.key(i);
37
+ if (key !== null) {
38
+ data[key] = window.localStorage.getItem(key) ?? '';
39
+ }
40
+ }
41
+ return data;
42
+ });
43
+ const session: SessionData = { cookies, localStorage };
44
+ const sessionPath = path.resolve(SESSION_FILE);
45
+ fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2));
46
+ console.log(`[${new Date().toISOString()}] Session saved to ${sessionPath}`);
47
+ }
48
+
49
+ export async function restoreSession(page: Page): Promise<void> {
50
+ const sessionPath = path.resolve(SESSION_FILE);
51
+ if (!fs.existsSync(sessionPath)) {
52
+ console.log(`[${new Date().toISOString()}] No session file found at ${sessionPath}`);
53
+ return;
54
+ }
55
+ const raw = fs.readFileSync(sessionPath, 'utf-8');
56
+ const session: SessionData = JSON.parse(raw);
57
+
58
+ await page.goto('https://onlyfans.com', { waitUntil: 'domcontentloaded' });
59
+ await page.setCookie(...session.cookies);
60
+
61
+ await page.evaluate((lsData: Record<string, string>) => {
62
+ for (const [key, value] of Object.entries(lsData)) {
63
+ window.localStorage.setItem(key, value);
64
+ }
65
+ }, session.localStorage);
66
+
67
+ console.log(`[${new Date().toISOString()}] Session restored from ${sessionPath}`);
68
+ }
69
+
70
+ export async function sessionFileExists(): Promise<boolean> {
71
+ return fs.existsSync(path.resolve(SESSION_FILE));
72
+ }
73
+
74
+ export async function ensureLoggedIn(browser: Browser): Promise<Page> {
75
+ const page = await browser.newPage();
76
+ await page.setViewport({ width: 1280, height: 900 });
77
+ await page.setUserAgent(
78
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
79
+ );
80
+
81
+ if (await sessionFileExists()) {
82
+ console.log(`[${new Date().toISOString()}] Restoring existing session...`);
83
+ await restoreSession(page);
84
+ await page.goto('https://onlyfans.com/my/chats', { waitUntil: 'networkidle2', timeout: 30000 });
85
+
86
+ const isLoggedIn = await checkLoggedIn(page);
87
+ if (isLoggedIn) {
88
+ console.log(`[${new Date().toISOString()}] Session valid — logged in`);
89
+ return page;
90
+ }
91
+ console.log(`[${new Date().toISOString()}] Session invalid or expired`);
92
+ }
93
+
94
+ // Need to log in manually
95
+ await page.goto('https://onlyfans.com/login', { waitUntil: 'networkidle2', timeout: 30000 });
96
+ console.log(`[${new Date().toISOString()}] Waiting for manual login at https://onlyfans.com/login ...`);
97
+
98
+ // Wait for navigation away from /login (indicates successful login)
99
+ await page.waitForFunction(
100
+ () => !window.location.href.includes('/login'),
101
+ { timeout: 300000 } // 5 minute timeout for human to log in
102
+ );
103
+
104
+ console.log(`[${new Date().toISOString()}] Login detected — saving session`);
105
+ await saveSession(page);
106
+
107
+ return page;
108
+ }
109
+
110
+ async function checkLoggedIn(page: Page): Promise<boolean> {
111
+ try {
112
+ const result = await page.evaluate(() => {
113
+ return (
114
+ document.querySelector('.b-header__user') !== null ||
115
+ document.querySelector('[data-type="messages"]') !== null ||
116
+ document.querySelector('.g-header__left .b-ddmenu') !== null ||
117
+ document.querySelector('.b-chats') !== null ||
118
+ document.querySelector('.l-sidebar__user') !== null
119
+ );
120
+ });
121
+ return result;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ export async function takeErrorScreenshot(page: Page): Promise<void> {
128
+ try {
129
+ await page.screenshot({ path: '/tmp/of_login_required.png', fullPage: true });
130
+ console.log(`[${new Date().toISOString()}] Screenshot saved to /tmp/of_login_required.png`);
131
+ } catch (err) {
132
+ console.error(`[${new Date().toISOString()}] Failed to take screenshot:`, err);
133
+ }
134
+ }
@@ -0,0 +1,104 @@
1
+ import type { Page } from 'puppeteer';
2
+
3
+ export interface Financials {
4
+ username: string;
5
+ totalTips: string;
6
+ totalPurchases: string;
7
+ subscriptionStatus: string;
8
+ lastPaymentDate: string;
9
+ lifetimeValue: string;
10
+ updatedAt: string;
11
+ }
12
+
13
+ export async function scrapeFinancials(page: Page, username: string, threadId: string): Promise<Financials | null> {
14
+ console.log(`[${new Date().toISOString()}] Fetching financials for @${username} (thread ${threadId})`);
15
+
16
+ const chatUrl = `https://onlyfans.com/my/chats/chat/${threadId}`;
17
+ try {
18
+ await page.goto(chatUrl, { waitUntil: 'networkidle2', timeout: 30000 });
19
+ } catch (err) {
20
+ console.error(`[${new Date().toISOString()}] Failed to navigate to thread for financials:`, err);
21
+ return null;
22
+ }
23
+
24
+ // Try to open the statistics/info panel
25
+ try {
26
+ const statsBtnSelectors = [
27
+ 'button[class*="statistic"]',
28
+ 'button[class*="stat"]',
29
+ '[data-type="stats"]',
30
+ 'button[title*="Statistic"]',
31
+ 'button[aria-label*="statistic"]',
32
+ '.b-chat__info-btn',
33
+ '[class*="infoBtn"]',
34
+ ];
35
+ for (const sel of statsBtnSelectors) {
36
+ const btn = await page.$(sel);
37
+ if (btn) {
38
+ await btn.click();
39
+ await new Promise((r) => setTimeout(r, 1500));
40
+ break;
41
+ }
42
+ }
43
+ } catch {
44
+ // Stats panel may already be visible or not available
45
+ }
46
+
47
+ const financials = await page.evaluate((uname: string): Financials => {
48
+ const getText = (selectors: string[]): string => {
49
+ for (const sel of selectors) {
50
+ const el = document.querySelector(sel);
51
+ if (el?.textContent?.trim()) return el.textContent.trim();
52
+ }
53
+ return '0';
54
+ };
55
+
56
+ const totalTips = getText([
57
+ '[class*="tips"] [class*="amount"]',
58
+ '[class*="tip"] [class*="total"]',
59
+ '[data-type="tips"]',
60
+ '.b-stat__tips',
61
+ ]);
62
+
63
+ const totalPurchases = getText([
64
+ '[class*="purchase"] [class*="amount"]',
65
+ '[class*="purchase"] [class*="total"]',
66
+ '[data-type="purchase"]',
67
+ '.b-stat__purchases',
68
+ ]);
69
+
70
+ const subscriptionStatus = getText([
71
+ '[class*="subscription"] [class*="status"]',
72
+ '[class*="subscriptionStatus"]',
73
+ '[data-type="subscriptionStatus"]',
74
+ ]) || 'unknown';
75
+
76
+ const lastPaymentDate = getText([
77
+ '[class*="lastPayment"]',
78
+ '[class*="last-payment"]',
79
+ '[data-type="lastPayment"]',
80
+ ]);
81
+
82
+ const lifetimeValue = getText([
83
+ '[class*="lifetimeValue"]',
84
+ '[class*="lifetime"]',
85
+ '[class*="totalSpent"]',
86
+ '[data-type="lifetimeValue"]',
87
+ ]);
88
+
89
+ return {
90
+ username: uname,
91
+ totalTips,
92
+ totalPurchases,
93
+ subscriptionStatus,
94
+ lastPaymentDate,
95
+ lifetimeValue,
96
+ updatedAt: new Date().toISOString(),
97
+ };
98
+ }, username);
99
+
100
+ console.log(
101
+ `[${new Date().toISOString()}] Financials for @${username}: tips=${financials.totalTips}, purchases=${financials.totalPurchases}`
102
+ );
103
+ return financials;
104
+ }