1import{k as Ae,r as u,X as be,A as Ye,l as r,o as n,q as e,m as me,D as b,K as L,F as K,n as ge,t as i,p as te,u as E,v as ce,W as Et,d as At,f as q,a0 as Nt,a1 as Tt,a2 as Dt,a3 as Rt,L as Ee,N as R,O as j,H as st,S as Ut,V as qe,B as Mt,I as Ze,R as et,a4 as Q,Q as Ft,w as Ve,x as Pt,a5 as Lt,E as $t}from"./vendor-D0Q8KstH.js";import{d as fe,b as nt,u as rt,e as ot,g as Bt,a as re}from"./index-DQqGazYE.js";import{s as Ie}from"./subscriptions-Cu18K4xM.js";import{N as Gt}from"./NavigationBar-B81tMCza.js";import{C as qt}from"./ChatAppSetupCard-whnvbLpL.js";import{c as Oe}from"./chat-1141wjaq.js";import{w as Vt}from"./x402-DMAZw0Lu.js";import"./sharedText512-eMELDve_.js";const je={getBillingInfo(){return fe.get("/billing/info/")},createCryptoPayment(o,p){const c={amount:o};return p&&(c.user_id=p),fe.post("/billing/crypto/create-payment/",c)},checkPaymentStatus(o){return fe.get(`/billing/crypto/status/${o}/`)},getWalletAddress(){return fe.get("/billing/wallet/")},setWalletAddress(o){return fe.put("/billing/wallet/",{wallet_address:o})}},Wt=` 2 // -- tiny utils ---------------------------------------------------------- 3 function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } 4 function hash(str) { 5 var h = 5381; 6 for (var i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) | 0; 7 return (h >>> 0).toString(36); 8 } 9 function log() { 10 if (!CONFIG.DEBUG) return; 11 var a = ['%c[bridge]', 'color:#4f6bed'].concat([].slice.call(arguments)); 12 console.log.apply(console, a); 13 } 14 async function waitFor(fn, timeoutMs, stepMs) { 15 var end = Date.now() + (timeoutMs || 5000); 16 while (Date.now() < end) { if (fn()) return true; await sleep(stepMs || 200); } 17 return false; 18 } 19 20 // -- keep the SPA "awake" in a background tab ----------------------------- 21 // Fansly/OnlyFans pause live message + conversation-list updates when the tab 22 // is hidden (Page Visibility / focus), so a message from a NEW user never 23 // appears until a manual reload. Spoof "always visible + focused" so the site 24 // keeps its websocket alive and keeps rendering new threads. No reloads. 25 function keepAwake() { 26 try { 27 var visible = function () { return false; }; 28 var state = function () { return 'visible'; }; 29 Object.defineProperty(document, 'hidden', { get: visible, configurable: true }); 30 Object.defineProperty(document, 'visibilityState', { get: state, configurable: true }); 31 try { Object.defineProperty(document, 'webkitHidden', { get: visible, configurable: true }); } catch (e) {} 32 try { Object.defineProperty(document, 'webkitVisibilityState', { get: state, configurable: true }); } catch (e) {} 33 document.hasFocus = function () { return true; }; 34 // visibilitychange is neutralised by the getter above (handlers read 35 // document.hidden === false). Block the focus-loss / lifecycle signals 36 // that some SPAs also pause on, without touching visibilitychange so a 37 // synthetic "resume" can still get through. 38 ['blur', 'pagehide', 'freeze'].forEach(function (t) { 39 window.addEventListener(t, function (e) { e.stopImmediatePropagation(); }, true); 40 }); 41 // Nudge an already-backgrounded SPA to re-evaluate as visible.
42 try { document.dispatchEvent(new Event('visibilitychange')); } catch (e) {} 43 try { window.dispatchEvent(new Event('focus')); } catch (e) {} 44 log('keep-awake: visibility + focus spoofed'); 45 } catch (e) { log('keep-awake failed', e); } 46 } 47 keepAwake(); 48 49 // Unread count from the tab title (e.g. "(3) Messages"). Updates over the 50 // websocket even when the list DOM lags â a live signal that new mail arrived. 51 var _lastUnread = -1; 52 function titleUnread() { 53 var m = (document.title || '').match(/\\((\\d+)\\)/); 54 return m ? parseInt(m[1], 10) : 0; 55 } 56 function checkUnread() { 57 var u = titleUnread(); 58 if (u !== _lastUnread) { 59 if (u > _lastUnread && _lastUnread >= 0) log('tab-title unread rose to', u, '(new message)'); 60 _lastUnread = u; 61 } 62 return u; 63 } 64 65 // -- persistence (survives reloads so we don't re-send on refresh) -------- 66 function loadSet(name) { 67 try { return new Set(JSON.parse(GM_getValue(name, '[]'))); } 68 catch (e) { return new Set(); } 69 } 70 function saveSet(name, set, cap) { 71 var arr = Array.from(set); 72 if (cap && arr.length > cap) arr = arr.slice(arr.length - cap); 73 try { GM_setValue(name, JSON.stringify(arr)); } catch (e) {} 74 } 75 76 // Storage namespace is scoped to the TOKEN: regenerating the bridge token 77 // (e.g. after purging rooms server-side) resets the dedupe state so history 78 // is re-delivered into the fresh rooms. Same token â state survives reloads. 79 var NS = CONFIG.PLATFORM + ':' + hash(CONFIG.TOKEN || ''); 80 var seen = loadSet('seen:' + NS);
80 // delivered message keys 81 var seenMedia = loadSet('seenMedia:' + NS); // delivered image keys 82 var backfilled = loadSet('backfilled:' + NS); // conv ids fully scrolled 83 var queue = []; 84 85 // -- diagnostics --------------------------------------------------------- 86 // Several selectors here are still guesses (which class marks a Fansly row 87 // unread, where the contact name lives) and the interesting moments â a row 88 // flipping to unread, a failed open, an HTTP error â happen unattended. So 89 // the runtime records them AS THEY HAPPEN: a rolling console log for the live 90 // view, plus a small set of FINDINGS persisted to GM storage, which survive 91 // a /refresh and are still there tomorrow. In the console: 92 // __bridge.help() what's available 93 // __bridge.report() everything gathered, ready to copy out 94 var Diag = { 95 ring: [], // recent events, this session only 96 onceKeys: {}, 97 findings: {}, // kind -> [{at, data}], persisted 98 started: new Date().toISOString(), 99 load: function () { 100 try { Diag.findings = JSON.parse(GM_getValue('diag:' + NS, '{}')) || {}; } 101 catch (e) { Diag.findings = {}; } 102 }, 103 persist: function () { 104 try { GM_setValue('diag:' + NS, JSON.stringify(Diag.findings)); } catch (e) {} 105 }, 106 // Keep the first few examples of a KIND of observation, permanently. Capped 107 // so a recurring condition can't grow storage without bound. 108 finding: function (kind, data, cap) { 109 var list = Diag.findings[kind] || (Diag.findings[kind] = []); 110 if (list.length >= (cap || 3)) return false; 111 list.push({ at: new Date().toISOString(), url: location.pathname, data: data }); 112 Diag.persist(); 113 console.log('%c[bridge] FINDING ' + kind, 'color:#0a0;font-weight:bold', data); 114 return true; 115 }, 116 record: function (tag, data) { 117 Diag.ring.push({ t: new Date().toISOString(), tag: tag, data: data }); 118 if (Diag.ring.length > 400) Diag.ring.shift(); 119 if (CONFIG.VERBOSE) log(tag, data); 120 }, 121 // Log/record something only the first time it's seen (per session). 122 once: function (key, tag, data) { 123 if (Diag.onceKeys[key]) return false; 124 Diag.onceKeys[key] = 1; 125 Diag.record(tag, data); 126 return true; 127 }, 128 // Repeat-suppression. A pending command that can't open retries on every 129 // 5s poll; logging that for half an hour buries everything else. Log it, 130 // hold it down, then report how many were suppressed when it recurs. 131 _lastAt: {}, 132 _suppressed: {}, 133 throttled: function (key, ms) { 134 var now = Date.now(); 135 if (now - (Diag._lastAt[key] || 0) < (ms || CONFIG.DIAG_THROTTLE_MS)) { 136 Diag._suppressed[key] = (Diag._suppressed[key] || 0) + 1; 137 return true; 138 } 139 Diag._lastAt[key] = now; 140 return false; 141 }, 142 // Loud: something went wrong and is worth keeping. 143 problem: function (kind, data, cap) { 144 if (!Diag.throttled('problem:' + kind)) { 145 var n = Diag._suppressed['problem:' + kind] || 0; 146 Diag._suppressed['problem:' + kind] = 0; 147 Diag.record( 148 'PROBLEM ' + kind + (n ? ' (' + n + ' more since the last one)' : ''), data, 149 ); 150 } 151 Diag.finding('problem:' + kind, data, cap || 5); 152 }, 153 }; 154 Diag.load(); 155 156 // -- DOM description helpers (used only for diagnostics) ----------------- 157 function classesOf(el) { 158 var out = []; 159 try { 160 (el.getAttribute('class') || '').split(/\\s+/).forEach(function (c) { if (c) out.push(c); }); 161 } catch (e) {} 162 return out; 163 } 164 // Every class on an element AND its descendants. The unread marker is 165 // somewhere in here â we just don't know its name yet, so capture the lot and 166 // let a before/after diff point at it. 167 function classSet(el) { 168 var out = []; 169 try { 170 classesOf(el).forEach(function (c) { if (out.indexOf(c) === -1) out.push(c); }); 171 el.querySelectorAll('*').forEach(function (n) { 172 classesOf(n).forEach(function (c) { if (out.indexOf(c) === -1) out.push(c); }); 173 }); 174 } catch (e) {} 175 return out; 176 } 177 // Leaf text nodes with their classes â reveals where the contact name, the 178 // preview line and any unread badge/count actually live. 179 function leafTexts(el, max) { 180 var out = []; 181 try { 182 el.querySelectorAll('*').forEach(function (n) { 183 if (out.length >= (max || 14)) return; 184 if (n.children && n.children.length) return; 185 var t = (n.textContent || '').replace(/\\s+/g, ' ').trim(); 186 if (!t) return; 187 out.push({ cls: (n.getAttribute('class') || '').slice(0, 60), text: t.slice(0, 48) }); 188 }); 189 } catch (e) {} 190 return out; 191 } 192 function rowHtml(el, max) { 193 try { return (el.outerHTML || '').slice(0, max || 900); } catch (e) { return ''; } 194 } 195 function describeRow(el) { 196 return { classes: classSet(el), leaves: leafTexts(el), html: rowHtml(el) }; 197 } 198 199 // Class-set snapshot per conversation row, so the moment a row changes we can 200 // diff what was ADDED (â the unread marker) and what was REMOVED when it is 201 // opened again (â the same marker, confirmed from the other direction). 202 var rowClassCache = {}; 203 function classDiff(id, el) { 204 if (!el) return null; 205 var now = classSet(el); 206 var prev = rowClassCache[id]; 207 rowClassCache[id] = now; 208 if (!prev) return null; 209 return { 210 added: now.filter(function (x) { return prev.indexOf(x) === -1; }), 211 removed: prev.filter(function (x) { return now.indexOf(x) === -1; }), 212 }; 213 } 214 215 // -- images --------------------------------------------------------------
216 // CDN URLs are signed (query string changes every page load), so dedupe and 217 // keys use the URL PATH only â that part is stable per media item. 218 function imagePath(src) { 219 try { return new URL(src, location.href).pathname; } catch (e) { return src; } 220 } 221 function guessImageType(path) { 222 var m = (path || '').toLowerCase().match(/\\.(png|gif|webp|avif|jpe?g)(?:$|[?#])/); 223 if (!m) return 'image/jpeg'; 224 var ext = m[1] === 'jpg' ? 'jpeg' : m[1]; 225 return 'image/' + ext; 226 } 227 // Collect real chat-media <img>s inside a message node: skip avatars, emoji 228 // and other tiny decorative images. 229 function collectMessageImages(root) { 230 // Never throws â this runs inside extractMessages, and an error on one 231 // odd <img> must not take down text extraction for the whole chat. 232 var out = [], have = {}; 233 // Quote-reply blocks belong to the quoted ORIGINAL message â their images 234 // must not be re-collected under the reply (Scraper.QUOTE_WRAPPER). 235 var excl = '[class*="avatar" i], [class*="emoji" i]'; 236 if (typeof Scraper !== 'undefined' && Scraper.QUOTE_WRAPPER) { 237 excl += ', ' + Scraper.QUOTE_WRAPPER; 238 } 239 try { 240 root.querySelectorAll('img').forEach(function (img) { 241 try { 242 var src = img.currentSrc || img.src || ''; 243 if (!src || src.indexOf('data:') === 0) return; 244 if (img.closest(excl)) return; 245 var w = img.naturalWidth || img.width || 0; 246 var h = img.naturalHeight || img.height || 0; 247 if (w && h && (w < 64 || h < 64)) return; // emoji / icons 248 var p = imagePath(src); 249 if (have[p]) return; 250 have[p] = 1; 251 out.push({ url: src, path: p, name: (p.split('/').pop() || 'image') }); 252 } catch (e) { log('image collect error', e); } 253 }); 254 } catch (e) { log('image collect error', e); } 255 return out; 256 } 257 258 // Fetch image bytes with the browser's own session (the backend cannot reach 259 // the platforms' auth'd/signed CDN URLs) and forward them to the bridge. 260 function fetchViaPage(url, cb) { 261 // In-page fetch â used for page-local blob: URLs and as a CORS-permitting 262 // fallback when GM_xmlhttpRequest is unavailable/denied for the host. 263 try { 264 fetch(url, { credentials: 'include' }) 265 .then(function (r) { return r.ok ? r.blob() : null; }) 266 .then(function (b) { cb(b); }, function () { cb(null); }); 267 } catch (e) { cb(null); } 268 } 269 function fetchImageBlob(url, cb) { 270 // blob: URLs are page-local â GM_xmlhttpRequest can't fetch them. 271 if (url.indexOf('blob:') === 0) { fetchViaPage(url, cb); return; } 272 try { 273 GM_xmlhttpRequest({ 274 method: 'GET', 275 url: url, 276 responseType: 'blob', 277 onload: function (res) { 278 cb(res.status >= 200 && res.status < 300 ? res.response : null); 279 }, 280 onerror: function () { fetchViaPage(url, cb); }, 281 }); 282 } catch (e) { 283 // Some managers THROW here (e.g. host not in @connect) â never let that 284 // propagate into the capture loop; try the page's own fetch instead. 285 log('GM image fetch threw', e); 286 fetchViaPage(url, cb); 287 } 288 } 289 function fetchBlobAsync(url) { 290 return new Promise(function (resolve) { fetchImageBlob(url, resolve); }); 291 } 292 293 // Signature of a conversation-list row, used to decide whether an ALREADY 294 // unread thread has received something new. Digits are stripped so the 295 // relative timestamp ticking over ("2m" â "3m") doesn't read as a new 296 // message, while a changed preview line does. 297 function rowSignature(el) { 298 try { 299 var t = (el.innerText || el.textContent || '') 300 .replace(/\\d+/g, '').replace(/\\s+/g, ' ').trim(); 301 return hash(t); 302 } catch (e) { return ''; } 303 } 304 305 // -- ordered delivery ---------------------------------------------------- 306 // ONE queue carries text and images in capture (DOM) order, drained 307 // strictly sequentially: an image message blocks the queue until its 308 // upload completes. The old parallel text/media pipelines delivered 309 // whenever their POSTs happened to finish, which scrambled the room 310 // timeline â Matrix orders events by arrival, not by any timestamp we send. 311 // Runs of consecutive text-only messages still batch into a single POST. 312 var TEXT_BATCH_MAX = 40; 313 314 function deliverBatch(msgs) { 315 var chatWith = msgs[0].chatWith; 316 if (CONFIG.DRY_RUN) { 317 log('DRY_RUN â would POST', chatWith, msgs); 318 setStatus('dry-run: ' + msgs.length + ' msg (not sent)'); 319 return Promise.resolve(true); 320 } 321 return new Promise(function (resolve) {
322 try { 323 GM_xmlhttpRequest({ 324 method: 'POST', 325 url: CONFIG.INGEST_URL, 326 headers: { 'Content-Type': 'application/json', 'X-Bridge-Token': CONFIG.TOKEN }, 327 data: JSON.stringify({ 328 platform: CONFIG.PLATFORM, 329 chatWith: chatWith, 330 conversationId: msgs[0].convId || '', 331 messages: msgs.map(function (m) { 332 return { sender: m.sender, text: m.text, time: m.time, key: m.key }; 333 }), 334 }), 335 onload: function (res) { 336 var ok = res.status >= 200 && res.status < 300; 337 setStatus(ok ? 'synced ' + msgs.length + ' msg' : 'sync error ' + res.status); 338 log('POST', chatWith, res.status, res.responseText); 339 if (!ok) { 340 Diag.problem('ingest-http', { 341 status: res.status, chatWith: chatWith, count: msgs.length, 342 reply: (res.responseText || '').slice(0, 300), 343 }); 344 } 345 resolve(ok); 346 }, 347 onerror: function () { 348 setStatus('network error'); 349 Diag.problem('ingest-network', { chatWith: chatWith, count: msgs.length }); 350 resolve(false); 351 }, 352 }); 353 } catch (e) { log('text POST threw', e); resolve(false); } 354 }); 355 } 356 357 async function uploadImageOrdered(m, img) { 358 if (CONFIG.DRY_RUN) { 359 log('DRY_RUN â would upload image', img.path, 'from', m.sender); 360 return true; 361 } 362 var blob = await fetchBlobAsync(img.url); 363 if (!blob || !blob.size) { 364 // Usually a @connect host missing from the header, or a CDN URL that has 365 // already expired â both worth keeping, they're invisible otherwise. 366 Diag.problem('image-fetch-failed', { 367 path: img.path, 368 host: (function () { try { return new URL(img.url, location.href).host; } catch (e) { return ''; } })(), 369 }); 370 return false; 371 } 372 var type = (blob.type && blob.type.indexOf('image/') === 0) 373 ? blob.type : guessImageType(img.path); 374 var body = new Blob([blob], { type: type }); 375 var name = img.name || 'image'; 376 var fd = new FormData(); 377 fd.append('platform', CONFIG.PLATFORM); 378 fd.append('chatWith', m.chatWith); 379 fd.append('conversationId', m.convId || ''); 380 fd.append('sender', m.sender); 381 fd.append('key', img.mkey); 382 fd.append('filename', name); 383 fd.append('image', body, name); 384 return new Promise(function (resolve) { 385 try { 386 GM_xmlhttpRequest({ 387 method: 'POST', 388 url: CONFIG.MEDIA_URL, 389 headers: { 'X-Bridge-Token': CONFIG.TOKEN }, 390 data: fd, 391 onload: function (res) { 392 var ok = res.status >= 200 && res.status < 300; 393 setStatus(ok ? 'synced image' : 'image sync error ' + res.status); 394 log('POST image', img.path, res.status); 395 if (!ok) { 396 Diag.problem('media-http', { 397 status: res.status, path: img.path, type: type, bytes: blob.size, 398 reply: (res.responseText || '').slice(0, 300), 399 }); 400 } 401 resolve(ok); 402 }, 403 onerror: function () { 404 setStatus('image network error'); 405 Diag.problem('media-network', { path: img.path }); 406 resolve(false); 407 }, 408 }); 409 } catch (e) { log('image upload threw', e); resolve(false); } 410 }); 411 } 412 413 var draining = false; 414 async function drainQueue() { 415 if (draining) return; 416 draining = true; 417 try { 418 while (queue.length) { 419 var item = queue[0]; 420 var hasImages = item.images && item.images.length; 421 if (item.text && !hasImages) { 422 // Batch the run of consecutive text-only messages for this chat. 423 var batch = []; 424 while (queue.length && batch.length < TEXT_BATCH_MAX) { 425 var n = queue[0]; 426 if (!n.text || (n.images && n.images.length) || n.chatWith !== item.chatWith) break; 427 batch.push(queue.shift()); 428 } 429 if (!(await deliverBatch(batch))) { 430 queue = batch.concat(queue); // keep order; retry next round 431 break; 432 } 433 continue; 434 } 435 // Image-bearing (or media-only) message: caption first, then each 436 // image, all completed before the next queue item goes out. 437 queue.shift(); 438 if (item.text && !(await deliverBatch([item]))) { 439 queue.unshift(item); 440 break; 441 } 442 var imgs = item.images || []; 443 for (var i = 0; i < imgs.length; i++) { 444 var ok = false;
445 try { ok = await uploadImageOrdered(item, imgs[i]); } 446 catch (e) { log('image deliver error', e); } 447 if (!ok) seenMedia.delete(imgs[i].mkey); // retro sweep retries later 448 } 449 saveSet('seenMedia:' + NS, seenMedia, 5000); 450 } 451 } catch (e) { log('drain error', e); } 452 finally { draining = false; } 453 } 454 455 // -- capture the open conversation -------------------------------------- 456 var mediaStalls = {}; // message key -> scans spent waiting for its media 457 function capture() { 458 var msgs; 459 try { msgs = Scraper.extractMessages(); } 460 catch (e) { 461 Diag.problem('extract-error', { 462 conv: Scraper.getConversationId(), error: String(e), 463 stack: (e && e.stack ? String(e.stack) : '').slice(0, 400), 464 }); 465 return 0; 466 } 467 // A thread is open but nothing came out of it. Usually an empty new 468 // conversation â but it's also exactly what a broken message selector looks 469 // like, and that would otherwise fail completely silently. 470 if (!msgs.length) { 471 Diag.problem('no-messages-extracted', { 472 conv: Scraper.getConversationId(), 473 scrollElFound: !!Scraper.getMessageScrollEl(), 474 nodesInScroller: (function () { 475 try { 476 var s = Scraper.getMessageScrollEl(); 477 return s ? s.querySelectorAll('*').length : 0; 478 } catch (e) { return -1; } 479 })(), 480 }); 481 return 0; 482 } 483 // Ordered-media gate: if an UNSEEN message's media hasn't rendered real 484 // URLs yet (lazy load), stop capturing AT that message â delivering 485 // anything after it would drop the image into the room out of order once 486 // it finally loads. Scroll it into view to force the lazy-loader and 487 // retry next scan; give up after MEDIA_WAIT_SCANS scans so one broken row 488 // can't stall the sync forever. Slow and in-order beats fast and scrambled. 489 var cut = msgs.length; 490 if (Scraper.isMessageReady) { 491 for (var gi = 0; gi < msgs.length; gi++) { 492 var gm = msgs[gi]; 493 if (seen.has(gm.key)) continue; 494 var ready = true; 495 try { ready = Scraper.isMessageReady(gm); } catch (e) {} 496 if (ready) { delete mediaStalls[gm.key]; continue; } 497 var stalls = (mediaStalls[gm.key] || 0) + 1; 498 mediaStalls[gm.key] = stalls; 499 if (stalls > CONFIG.MEDIA_WAIT_SCANS) { 500 Diag.problem('media-never-loaded', { 501 key: gm.key, conv: gm.convId, scans: stalls, 502 html: gm.el ? rowHtml(gm.el, 500) : '', 503 }); 504 continue; 505 } 506 try { if (gm.el) gm.el.scrollIntoView({ block: 'center' }); } catch (e) {} 507 setStatus('waiting for media to loadâ¦'); 508 log('waiting for media on', gm.key, '(scan ' + stalls + ')'); 509 cut = gi; 510 break; 511 } 512 } 513 var freshCount = 0; 514 msgs.slice(0, cut).forEach(function (m) { 515 // Claim this message's not-yet-delivered images now (released again on 516 // upload failure so the retro path below retries them). 517 var pending = []; 518 (m.images || []).forEach(function (img) { 519 var mkey = 'img:' + CONFIG.PLATFORM + ':' + (m.convId || '') + ':' + hash(img.path); 520 if (seenMedia.has(mkey)) return; 521 seenMedia.add(mkey); 522 pending.push({ url: img.url, path: img.path, name: img.name, mkey: mkey }); 523 }); 524 var isFresh = (m.text || (m.images && m.images.length)) && !seen.has(m.key); 525 if (isFresh) { 526 seen.add(m.key); 527 m.images = pending; 528 queue.push(m); 529 freshCount++; 530 } else if (pending.length) { 531 // Message already delivered but an image only just appeared (lazy 532 // load) or previously failed â send it now; its original timeline 533 // slot is gone, so append-at-end is the best we can do. 534 queue.push({ 535 chatWith: m.chatWith, convId: m.convId, sender: m.sender, 536 time: m.time, text: '', key: m.key + ':retro', images: pending, 537 }); 538 } 539 }); 540 if (freshCount) { 541 saveSet('seen:' + NS, seen, 5000); 542 setStatus('queued ' + freshCount + ' new'); 543 log('captured', freshCount, 'new of', msgs.length); 544 } else if (msgs.length && cut === msgs.length) { 545 // Make dedupe suppression visible â "nothing arrived" and "everything 546 // was already delivered" look identical without this. (Suppressed while 547 // the media gate is holding capture back â that's already reported.) 548 log('all', msgs.length, 'messages already delivered (dedupe) â regenerate the bridge token for a clean re-sync'); 549 } 550 return freshCount; 551 } 552 553 async function processCurrent() { 554 var convId = Scraper.getConversationId(); 555 if (!convId) { 556 // Normal idle state with AUTO_OPEN off: nothing is open, unread threads 557 // are reported to Matrix and wait there for a /read. 558 if (!CONFIG.AUTO_OPEN) setStatus('watching for unreadâ¦'); 559 else setStatus('open a conversationâ¦'); 560 return; 561 } 562 var scroll = Scraper.getMessageScrollEl(); 563 if (scroll && CONFIG.BACKFILL && !backfilled.has(convId)) { 564 setStatus('loading full historyâ¦');
565 try { await Scraper.backfill(scroll); } catch (e) { log('backfill error', e); } 566 backfilled.add(convId); 567 saveSet('backfilled:' + NS, backfilled); 568 } 569 capture(); 570 } 571 572 // -- outbound: Matrix reply -> platform ---------------------------------- 573 var outboxSeen = new Set(); // ids handled this session (avoid re-typing) 574 // Executed COMMAND ids, persisted: /refresh reloads the page, so an 575 // in-session-only guard would let a lost ack re-fire it in a reload loop. 576 var cmdDone = loadSet('cmdDone:' + NS); 577 var lastOutboxPoll = 0; 578 579 function fetchOutbox() { 580 return new Promise(function (resolve) { 581 GM_xmlhttpRequest({ 582 method: 'GET', 583 url: CONFIG.OUTBOX_URL, 584 headers: { 'X-Bridge-Token': CONFIG.TOKEN }, 585 onload: function (res) { 586 if (res.status < 200 || res.status >= 300) { 587 Diag.problem('outbox-http', { 588 status: res.status, reply: (res.responseText || '').slice(0, 300), 589 }); 590 resolve([]); 591 return; 592 } 593 var msgs = []; 594 try { msgs = (JSON.parse(res.responseText) || {}).messages || []; } 595 catch (e) { 596 Diag.problem('outbox-parse', { reply: (res.responseText || '').slice(0, 300) }); 597 resolve([]); 598 return; 599 } 600 // Keyed on the ids so a NEW item logs at once, while the same stuck 601 // item sitting in the queue doesn't reprint every poll. 602 var key = 'outbox:' + msgs.map(function (m) { return m.id; }).join(','); 603 if (msgs.length && !Diag.throttled(key)) { 604 Diag.record('outbox has work', msgs.map(function (m) { 605 return { 606 id: m.id, command: m.command || '(reply)', conv: m.conversation_id, 607 to: m.remote_username, stageOnly: !!m.stage_only, 608 bodyLen: (m.body || '').length, 609 }; 610 })); 611 } 612 resolve(msgs); 613 }, 614 onerror: function () { Diag.problem('outbox-network', {}); resolve([]); }, 615 }); 616 }); 617 } 618 // Awaitable: /refresh must not reload the page until its ack has landed, or 619 // the command comes back pending after the reload and loops forever. 620 function ackOutbox(sentIds, failedIds) { 621 return new Promise(function (resolve) { 622 try { 623 GM_xmlhttpRequest({ 624 method: 'POST', 625 url: CONFIG.OUTBOX_URL, 626 headers: { 'Content-Type': 'application/json', 'X-Bridge-Token': CONFIG.TOKEN }, 627 data: JSON.stringify({ sent: sentIds || [], failed: failedIds || [] }), 628 onload: function (res) { 629 var ok = res.status >= 200 && res.status < 300; 630 Diag.record('outbox ack', { sent: sentIds, failed: failedIds, status: res.status }); 631 // A lost ack is how a command comes back and runs twice â never 632 // silent, even though the local cmdDone guard also covers it. 633 if (!ok) Diag.problem('outbox-ack-http', { status: res.status, sent: sentIds }); 634 resolve(ok); 635 }, 636 onerror: function () { 637 Diag.problem('outbox-ack-network', { sent: sentIds, failed: failedIds }); 638 resolve(false); 639 }, 640 }); 641 } catch (e) { Diag.problem('outbox-ack-threw', { error: String(e) }); resolve(false); } 642 }); 643 } 644 645 // -- unread reporting (no reading!) -------------------------------------- 646 // Report which conversations the LIST shows as unread. Reading the list never 647 // marks anything read on the platform â opening a thread does, which is why 648 // that only happens on an explicit /read. 649 var lastUnreadSig = null; 650 var lastUnreadPost = 0; 651 function postUnread(items, total) { 652 return new Promise(function (resolve) { 653 try { 654 GM_xmlhttpRequest({ 655 method: 'POST', 656 url: CONFIG.UNREAD_URL, 657 headers: { 'Content-Type': 'application/json', 'X-Bridge-Token': CONFIG.TOKEN }, 658 data: JSON.stringify({ 659 platform: CONFIG.PLATFORM, total: total, conversations: items, 660 }), 661 onload: function (res) { 662 var ok = res.status >= 200 && res.status < 300; 663 Diag.record('unread report â server', { 664 unread: items.length, total: total, status: res.status, 665 reply: (res.responseText || '').slice(0, 200), 666 conversations: items, 667 }); 668 if (!ok) { 669 Diag.problem('unread-report-http', { 670 status: res.status, reply: (res.responseText || '').slice(0, 300), 671 }); 672 } 673 resolve(ok); 674 }, 675 onerror: function () { Diag.problem('unread-report-net
675work', {}); resolve(false); }, 676 }); 677 } catch (e) { log('unread report threw', e); resolve(false); } 678 }); 679 } 680 // Second unread signal. The platforms' own "unread" markers are class-name 681 // guesses (see Scraper._isUnread), so a row whose CONTENT changes while it is 682 // not the open thread is also treated as unread â that is almost always a new 683 // message landing in it. Sticky (the change is a one-off event) until the 684 // thread is opened. Survives a /refresh so a reload can't lose the flag. 685 var rowSigs = {}; 686 var changedUnread = loadSet('changedUnread:' + NS); 687 function trackRowChanges(convs) { 688 if (!CONFIG.UNREAD_FROM_CHANGES) return; 689 var dirty = false; 690 convs.forEach(function (c) { 691 if (!c.id) return; 692 var prev = rowSigs[c.id]; 693 rowSigs[c.id] = c.sig; 694 if (c.selected) { 695 // Row just became the OPEN thread: whatever classes disappear here are 696 // the platform's "read" transition â the same marker, seen in reverse. 697 var closeDiff = classDiff(c.id, c.el); 698 if (changedUnread.delete(c.id)) { 699 dirty = true; 700 if (closeDiff && (closeDiff.removed.length || closeDiff.added.length)) { 701 Diag.finding('read-transition-classes', { 702 id: c.id, removed: closeDiff.removed, added: closeDiff.added, 703 unreadGuessNow: c.unread, 704 }, 5); 705 } 706 } 707 return; 708 } 709 if (prev !== undefined && prev !== c.sig && !changedUnread.has(c.id)) { 710 // THE moment worth capturing: a closed row's content just changed, so a 711 // message almost certainly landed in it. Whatever classes appeared are 712 // the platform's unread marker â this is what Scraper._isUnread should 713 // be looking for. 714 var diff = classDiff(c.id, c.el); 715 Diag.record('unread transition (row changed while closed)', { 716 id: c.id, name: c.name, unreadGuess: c.unread, 717 classesAdded: diff ? diff.added : null, 718 classesRemoved: diff ? diff.removed : null, 719 }); 720 Diag.finding('unread-transition-classes', { 721 id: c.id, unreadGuess: c.unread, 722 added: diff ? diff.added : null, 723 removed: diff ? diff.removed : null, 724 leaves: leafTexts(c.el), 725 html: rowHtml(c.el), 726 }, 5); 727 // The selector guess disagreeing with an obvious new message is the 728 // clearest possible signal that _isUnread needs fixing. 729 if (!c.unread) { 730 Diag.finding('unread-marker-missed', { 731 id: c.id, added: diff ? diff.added : null, allClasses: classSet(c.el), 732 }, 5); 733 } 734 changedUnread.add(c.id); 735 dirty = true; 736 } else if (prev === undefined) { 737 classDiff(c.id, c.el); // prime the cache on first sight 738 } 739 }); 740 if (dirty) saveSet('changedUnread:' + NS, changedUnread, 500); 741 } 742 743 async function reportUnread(convs) { 744 if (!CONFIG.REPORT_UNREAD || !convs || !convs.length) return; 745 trackRowChanges(convs); 746 var items = []; 747 convs.forEach(function (c) { 748 if (!c.id) return; 749 if (!c.unread && !changedUnread.has(c.id)) return; 750 items.push({ id: String(c.id), name: c.name || '', sig: c.sig || '' }); 751 }); 752 var sig = items.map(function (i) { return i.id + '~' + i.sig; }).join('|'); 753 var now = Date.now(); 754 // Send on change; otherwise heartbeat occasionally so a dropped report 755 // (network blip) still converges. 756 if (sig === lastUnreadSig && now - lastUnreadPost < CONFIG.UNREAD_HEARTBEAT_MS) return; 757 lastUnreadSig = sig; 758 lastUnreadPost = now; 759 if (items.length) setStatus(items.length + ' unread (send /read in Matrix)'); 760 var ok = await postUnread(items, convs.length); 761 if (!ok) lastUnreadSig = null; // retry on the next scan 762 } 763 764 // Split a multi-line reply into separate platform messages (how Me actually 765 // chats). Blank lines are dropped. Disabled when CONFIG.SPLIT_LINES is false. 766 function splitOutboundLines(text) { 767 // NB: escapes must be doubled â this source lives in a JS template literal, 768 // so a lone \\n here would emit a real newline INSIDE the regex literal and 769 // break the whole userscript at parse time. 770 return String(text || '').split(/\\r?\\n/).map(function (l) { 771 return l.replace(/^\\s+|\\s+$/g, ''); 772 }).filter(function (l) { return l.length > 0; }); 773 } 774 function lineGapMs() { 775 var lo = CONFIG.LINE_GAP_MIN_MS || 2000; 776 var hi = CONFIG.LINE_GAP_MAX_MS || 5000; 777 if (hi < lo) hi = lo; 778 return lo + Math.floor(Math.random() * (hi - lo + 1)); 779 } 780 // Send (or stage) an outbox body. On real send, each non-empty line is its 781 // own message with a random 2â5s pause before the next Enter â so a 2-line 782 // suggestion becomes two bubbles, not one. Staging keeps the full text in 783 // the composer for review (no auto-submit, no inter-line delay). 784 async function sendOutboundBody(body, opts) { 785 var stageOnly = !!(opts && opts.stageOnly); 786 var split = CONFIG.SPLIT_LINES !== false; 787 var lines = split ? splitOutboundLines(body) : [String(body || '')]; 788 if (!lines.length) return false;
789 if (stageOnly || lines.length === 1) { 790 var one = stageOnly ? String(body || '') : lines[0]; 791 return await Scraper.sendMessage(one, opts); 792 } 793 var lastRes = false; 794 for (var li = 0; li < lines.length; li++) { 795 setStatus( 796 'outbound: line ' + (li + 1) + '/' + lines.length 797 + ' â ' + lines[li].slice(0, 40) 798 ); 799 try { lastRes = await Scraper.sendMessage(lines[li], opts); } 800 catch (e) { log('outbound line send error', e); return false; } 801 if (!lastRes) return false; 802 if (lastRes === 'dryrun' || lastRes === 'staged') return lastRes; 803 if (li < lines.length - 1) { 804 var gap = lineGapMs(); 805 setStatus('outbound: next line in ' + Math.round(gap / 1000) + 'sâ¦'); 806 log('outbound: waiting', gap, 'ms before line', li + 2); 807 await sleep(gap); 808 } 809 } 810 return lastRes; 811 } 812 813 // -- outbound: control commands (/read, /refresh) ------------------------- 814 // These carry no text for the platform â they drive the userscript itself. 815 // The creator types them in a bridge room; /read is what actually opens (and 816 // therefore marks read) that room's conversation. 817 async function runCommand(m) { 818 var cmd = String(m.command || '').toLowerCase(); 819 var label = m.remote_username || m.conversation_id || ''; 820 Diag.record('command received', { 821 id: m.id, command: cmd, conv: m.conversation_id, to: m.remote_username, 822 openNow: Scraper.getConversationId(), 823 }); 824 825 if (cmd === 'refresh') { 826 log('command: /refresh â reloading'); 827 setStatus('/refresh â reloading pageâ¦'); 828 // Mark done and ack BEFORE the reload lands, so the command can't come 829 // back pending and reload us again. 830 cmdDone.add(String(m.id)); 831 saveSet('cmdDone:' + NS, cmdDone, 200); 832 await ackOutbox([m.id], []); 833 await sleep(500); 834 location.reload(); 835 await sleep(30000); // page is tearing down â stop doing work 836 return; 837 } 838 839 if (cmd === 'read') { 840 if (!m.conversation_id || !Scraper.openConversation) { 841 Diag.problem('read-unusable', { 842 id: m.id, conv: m.conversation_id, 843 why: m.conversation_id ? 'scraper cannot open conversations' : 'no conversation id on the outbox row', 844 }); 845 outboxSeen.add(m.id); 846 await ackOutbox([], [m.id]); 847 return; 848 } 849 setStatus('/read â opening ' + label); 850 var opened = await Scraper.openConversation(m.conversation_id); 851 if (!opened) { 852 // Not in the loaded list â leave pending and retry on a later poll. 853 // Record what WAS in the list: if the id isn't there at all, the room's 854 // stored conversation_id is stale or the list needs scrolling. 855 var known = (Scraper.listConversations ? Scraper.listConversations() : []) 856 .map(function (c) { return c.id; }); 857 Diag.problem('read-open-failed', { 858 conv: m.conversation_id, 859 inList: known.indexOf(String(m.conversation_id)) !== -1, 860 listSize: known.length, listIds: known.slice(0, 30), 861 openNow: Scraper.getConversationId(), 862 }); 863 return; 864 } 865 if (Scraper.getConversationId() !== String(m.conversation_id)) { 866 Diag.problem('read-conversation-mismatch', { 867 wanted: String(m.conversation_id), got: Scraper.getConversationId(), 868 }); 869 return; 870 } 871 await sleep(1200); // let the thread render before capturing 872 if (changedUnread.delete(String(m.conversation_id))) { 873 saveSet('changedUnread:' + NS, changedUnread, 500); 874 } 875 var before = queue.length;
876 try { await processCurrent(); } 877 catch (e) { Diag.problem('read-capture-failed', { conv: m.conversation_id, error: String(e) }); } 878 Diag.record('/read synced', { 879 conv: m.conversation_id, queued: queue.length - before, 880 queueDepth: queue.length, seenTotal: seen.size, 881 }); 882 // Nothing new after opening an unread thread means either the messages 883 // were already delivered, or extraction is failing on this thread. 884 if (queue.length === before) { 885 Diag.finding('read-produced-nothing', { 886 conv: m.conversation_id, 887 messagesOnScreen: (function () { 888 try { return Scraper.extractMessages().length; } catch (e) { return 'extract error: ' + e; } 889 })(), 890 }, 5); 891 } 892 drainQueue(); 893 cmdDone.add(String(m.id)); 894 saveSet('cmdDone:' + NS, cmdDone, 200); 895 outboxSeen.add(m.id); 896 await ackOutbox([m.id], []); 897 setStatus('read + syncing ' + label); 898 return; 899 } 900 901 Diag.problem('unknown-command', { id: m.id, command: cmd }); 902 outboxSeen.add(m.id); 903 await ackOutbox([], [m.id]); 904 } 905 906 async function drainOutbox() { 907 if (Date.now() - lastOutboxPoll < CONFIG.OUTBOX_POLL_MS) return; 908 lastOutboxPoll = Date.now(); 909 910 var msgs = await fetchOutbox(); 911 for (var i = 0; i < msgs.length; i++) { 912 var m = msgs[i]; 913 if (outboxSeen.has(m.id) || cmdDone.has(String(m.id))) continue; 914 if (m.command) { await runCommand(m); continue; } 915 // Replies (text to type) â commands above run even with OUTBOUND off. 916 if (!CONFIG.OUTBOUND || !Scraper.openConversation || !Scraper.sendMessage) continue; 917 if (!m.conversation_id) { outboxSeen.add(m.id); ackOutbox([], [m.id]); continue; } 918 919 setStatus('outbound: opening ' + (m.remote_username || m.conversation_id)); 920 var opened = await Scraper.openConversation(m.conversation_id); 921 if (!opened) { 922 // Not in the loaded list â leave pending, retry a later poll. 923 Diag.problem('outbound-open-failed', { 924 conv: m.conversation_id, to: m.remote_username, 925 openNow: Scraper.getConversationId(), 926 }); 927 continue; 928 } 929 // Guard: only ever send while the intended conversation is actually open. 930 if (Scraper.getConversationId() !== String(m.conversation_id)) { 931 log('outbound: conversation mismatch, skipping', m.conversation_id); 932 continue; 933 } 934 935 var res; 936 // Per-message stage_only (from the assistant bot's /chat or reaction 937 // chips): type the reply into the composer but DON'T submit â the creator 938 // reviews and sends by hand. Real sends split multi-line bodies. 939 try { res = await sendOutboundBody(m.body, { stageOnly: !!m.stage_only }); } 940 catch (e) { log('outbound send error', e); res = false; } 941 942 outboxSeen.add(m.id); 943 if (res === 'staged') { 944 // Typed, not submitted. Ack as done so a page reload doesn't re-type 945 // over the creator's in-progress edits. 946 ackOutbox([m.id], []); 947 setStatus('staged (typed, not sent) â ' + (m.remote_username || m.conversation_id)); 948 log('outbound STAGED: typed into', m.conversation_id, JSON.stringify(m.body)); 949 } else if (res === 'dryrun') { 950 setStatus('DRY-RUN typed reply (not sent) â ' + (m.remote_username || m.conversation_id)); 951 log('outbound DRY-RUN: typed into', m.conversation_id, JSON.stringify(m.body)); 952 } else if (res) { 953 ackOutbox([m.id], []); 954 var nLines = splitOutboundLines(m.body).length; 955 setStatus( 956 (nLines > 1 ? ('sent ' + nLines + ' lines') : 'sent reply') 957 + ' â ' + (m.remote_username || m.conversation_id) 958 ); 959 } else { 960 ackOutbox([], [m.id]); 961 setStatus('reply FAILED â ' + (m.remote_username || m.conversation_id)); 962 } 963 } 964 } 965 966 // -- main loop ----------------------------------------------------------- 967 var visitedThisRun = new Set(); 968 async function loop() { 969 while (true) { 970 try { 971 await drainOutbox(); 972 var unread = checkUnread(); 973 var convs = Scraper.listConversations ? Scraper.listConversations() : []; 974 if (convs.length) { 975 if (CONFIG.DEBUG) Scraper.debugConversations && Scraper.debugConversations(convs); 976 // Diagnostic: the tab title reports unread mail but the conversation 977 // list shows none â the list DOM did not live-update even with 978 // keep-awake, so a new thread is being missed. (Report to tune a 979 // targeted list-resync if this ever prints.) 980 var unreadInList = convs.filter(function (c) { return c.unread; }).length; 981 if (unread > 0 && unreadInList === 0) { 982 Diag.problem('unread-mismatch', { 983 titleSays: unread, listSays: 0, rows: convs.length, 984 changeDetected: changedUnread.size, 985 note: 'either the list DOM is stale, or _isUnread does not match this platform', 986 sample: convs[0] ? describeRow(convs[0].el) : null, 987 }, 3); 988 } 989 // Tell Matrix which threads are unread WITHOUT opening them. Opening 990 // is what marks a thread read on the platform, so it only ever 991 // happens on an explicit /read (or with AUTO_OPEN on, below). 992 await reportUnread(convs); 993 } 994 if (CONFIG.AUTO_OPEN && convs.length) { 995 // Prefer unread threads we haven't opened yet this run. 996 var next = convs.filter(function (c) { 997 return c.unread && !c.selected && !visitedThisRun.has(c.id); 998 })[0]; 999 // Bootstrap: sitting on the conversation LIST with nothing open and 1000 // nothing unread, there is no thread to cycle to, so the loop idles 1001 // on "open a conversationâ¦" forever. Open the top thread just to get 1002 // a conversation on screen â after this the normal unread cycle runs. 1003 if (!next && !Scraper.getConversationId() && convs.length) { 1004 next = convs.filter(function (c) { return !visitedThisRun.has(c.id); })[0]; 1005 if (next) log('bootstrap: no conversation open, opening first thread'); 1006 } 1007 if (next) { 1008 setStatus('opening unread threadâ¦'); 1009 visitedThisRun.add(next.id); 1010 await Scraper.selectConversation(next); 1011 await sleep(1500); 1012 } else if (visitedThisRun.size && !convs.some(function (c) { return c.unread; })) { 1013 // No unread left â reset so future new unread threads get picked up. 1014 visitedThisRun.clear(); 1015 } 1016 } 1017 await processCurrent(); 1018 drainQueue(); // deliberately not awaited â a long image backfill must 1019 // not stall scanning or the outbox; ordering is kept by 1020 // the queue itself, not by who calls the drain. 1021 heartbeat(convs); 1022 } catch (e) { 1023 Diag.problem('loop-error', { 1024 error: String(e), stack: (e && e.stack ? String(e.stack) : '').slice(0, 400), 1025 }, 10); 1026 } 1027 await sleep(CONFIG.SCAN_INTERVAL_MS); 1028 } 1029 } 1030 1031 // Periodic one-line summary so a console left open overnight still shows what 1032 // the bridge believed at any point â without printing on every 3s scan. 1033 var lastHeartbeat = 0; 1034 function heartbeat(convs) { 1035 if (Date.now() - lastHeartbeat < CONFIG.HEARTBEAT_MS) return; 1036 lastHeartbeat = Date.now(); 1037 Diag.record('heartbeat', { 1038 open: Scraper.getConversationId() || '(none)', 1039 rows: convs.length, 1040 unreadByMarker: convs.filter(function (c) { return c.unread; }).length, 1041 unreadByChange: changedUnread.size, 1042 titleUnread: titleUnread(), 1043 queued: queue.length, 1044 delivered: seen.size, 1045 findings: Object.keys(Diag.findings).length, 1046 }); 1047 } 1048 1049 // -- status pill --------------------------------------------------------- 1050 var pill; 1051 function setStatus(text) { 1052 if (!pill) { 1053 pill = document.createElement('div'); 1054 pill.style.cssText = 1055 'position:fixed;bottom:12px;right:12px;z-index:2147483647;background:#111;' + 1056 'color:#fff;font:12px/1.4 sans-serif;padding:6px 10px;border-radius:8px;' + 1057 'opacity:.85;pointer-events:none;max-width:260px;'; 1058 (document.body || document.documentElement).appendChild(pill); 1059 } 1060 var unread = titleUnread(); 1061 pill.textContent = 'Shared bridge' + (CONFIG.DRY_RUN ? ' (dry-run)' : '') 1062 + (unread ? ' [' + unread + ' unread]' : '') + ': ' + text; 1063 } 1064 1065 // -- console API --------------------------------------------------------- 1066 // Everything the runtime has learned, reachable by hand. Exposed on the PAGE 1067 // window (via unsafeWindow) so the devtools console reaches it without having 1068 // to switch to the userscript sandbox context. The bridge token is never put 1069 // on it; the tradeoff is that page scripts can call these too, which on the 1070 // creator's own logged-in tab is not a meaningful escalation. 1071 function safeConfig() { 1072 var out = {};
1073 Object.keys(CONFIG).forEach(function (k) { 1074 out[k] = k === 'TOKEN' ? '(redacted)' : CONFIG[k]; 1075 }); 1076 return out; 1077 } 1078 var api = { 1079 help: function () { 1080 console.log([ 1081 'Shared chat bridge v' + CONFIG.VERSION + ' â console commands:', 1082 ' __bridge.report() everything gathered (copy this out)', 1083 ' __bridge.findings() persisted findings (survive reloads)', 1084 ' __bridge.log() recent events, this session', 1085 ' __bridge.state() what the bridge believes right now', 1086 ' __bridge.rows() conversation list as the scraper sees it', 1087 ' __bridge.inspect(id) full DOM description of one list row', 1088 ' __bridge.messages() messages extracted from the open thread', 1089 ' __bridge.read(id) open + sync a conversation now (marks it read)', 1090 ' __bridge.forget() clear findings (keeps delivery state)', 1091 ' __bridge.resync() forget delivered keys â re-sends this thread', 1092 ].join('\\n')); 1093 return CONFIG.VERSION; 1094 }, 1095 config: safeConfig, 1096 findings: function () { return Diag.findings; }, 1097 log: function (n) { return Diag.ring.slice(-(n || 60)); }, 1098 state: function () { 1099 var convs = Scraper.listConversations ? Scraper.listConversations() : []; 1100 return { 1101 version: CONFIG.VERSION, 1102 platform: CONFIG.PLATFORM, 1103 startedAt: Diag.started, 1104 openConversation: Scraper.getConversationId() || '(none)', 1105 rows: convs.length, 1106 unreadByMarker: convs.filter(function (c) { return c.unread; }).map(function (c) { return c.id; }), 1107 unreadByChange: Array.from(changedUnread), 1108 titleUnread: titleUnread(), 1109 queueDepth: queue.length, 1110 deliveredKeys: seen.size, 1111 deliveredImages: seenMedia.size, 1112 commandsDone: Array.from(cmdDone), 1113 lastUnreadReport: lastUnreadPost ? new Date(lastUnreadPost).toISOString() : null, 1114 }; 1115 }, 1116 rows: function () { 1117 return (Scraper.listConversations ? Scraper.listConversations() : []).map(function (c) { 1118 return { 1119 id: c.id, name: c.name, selected: c.selected, 1120 unreadByMarker: c.unread, unreadByChange: changedUnread.has(c.id), sig: c.sig, 1121 }; 1122 }); 1123 }, 1124 inspect: function (id) { 1125 var convs = Scraper.listConversations ? Scraper.listConversations() : []; 1126 var row = convs.filter(function (c) { return String(c.id) === String(id); })[0]; 1127 return row ? describeRow(row.el) : 'no such row; try __bridge.rows()'; 1128 }, 1129 messages: function () { 1130 try { 1131 return Scraper.extractMessages().map(function (m) { 1132 return { 1133 key: m.key, sender: m.sender, time: m.time, 1134 text: (m.text || '').slice(0, 60), 1135 images: (m.images || []).length, delivered: seen.has(m.key), 1136 }; 1137 }); 1138 } catch (e) { return 'extract error: ' + e; } 1139 }, 1140 read: async function (id) { 1141 return await runCommand({ id: 'manual', command: 'read', conversation_id: String(id) }); 1142 }, 1143 forget: function () { 1144 Diag.findings = {}; 1145 Diag.persist(); 1146 Diag.onceKeys = {}; 1147 return 'findings cleared'; 1148 }, 1149 resync: function () { 1150 seen.clear(); 1151 saveSet('seen:' + NS, seen); 1152 return 'delivered-message state cleared â this thread will re-send on the next scan'; 1153 }, 1154 report: function () { 1155 var r = { 1156 version: CONFIG.VERSION, platform: CONFIG.PLATFORM, 1157 userAgent: (typeof navigator !== 'undefined' && navigator.userAgent) || '', 1158 config: safeConfig(), state: api.state(), findings: Diag.findings, 1159 recent: Diag.ring.slice(-80), 1160 }; 1161 console.log(r);
1162 try { console.log('%c[bridge] copy(__bridge.report()) puts this on the clipboard', 'color:#4f6bed'); } catch (e) {} 1163 return r; 1164 }, 1165 }; 1166 try { 1167 (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window).__bridge = api; 1168 } catch (e) { 1169 try { window.__bridge = api; } catch (e2) {} 1170 } 1171 1172 // Anything thrown outside the loop's own try (event handlers, async gaps) 1173 // would otherwise vanish into the console at 3am. 1174 try { 1175 window.addEventListener('error', function (ev) { 1176 if (!ev || !ev.message) return; 1177 Diag.problem('window-error', { 1178 message: String(ev.message).slice(0, 200), 1179 at: (ev.filename || '') + ':' + (ev.lineno || ''), 1180 }, 10); 1181 }); 1182 window.addEventListener('unhandledrejection', function (ev) { 1183 Diag.problem('unhandled-rejection', { 1184 reason: String((ev && ev.reason && (ev.reason.stack || ev.reason.message)) || (ev && ev.reason) || '').slice(0, 300), 1185 }, 10); 1186 }); 1187 } catch (e) {} 1188 1189 console.log( 1190 '%c[bridge] v' + CONFIG.VERSION + ' starting â type __bridge.help() for diagnostics', 1191 'color:#4f6bed;font-weight:bold', 1192 { 1193 platform: CONFIG.PLATFORM, 1194 autoOpen: CONFIG.AUTO_OPEN, 1195 reportUnread: CONFIG.REPORT_UNREAD, 1196 outbound: CONFIG.OUTBOUND && !CONFIG.OUTBOUND_DRYRUN, 1197 delivered: seen.size, 1198 pendingUnreadFromChanges: Array.from(changedUnread), 1199 }, 1200 ); 1201 if (Object.keys(Diag.findings).length) { 1202 console.log( 1203 '%c[bridge] findings carried over from earlier sessions â __bridge.findings()', 1204 'color:#0a0;font-weight:bold', 1205 Object.keys(Diag.findings).map(function (k) { 1206 return k + ' Ã' + Diag.findings[k].length; 1207 }), 1208 ); 1209 } 1210 1211 setStatus('startingâ¦'); 1212 loop(); 1213 setInterval(drainQueue, CONFIG.FLUSH_INTERVAL_MS); 1214`,Ht=` 1215 var Scraper = { 1216 getConversationId: function () { 1217 var m = location.pathname.match(/\\/messages\\/(\\d+)/); 1218 return m ? m[1] : ''; 1219 }, 1220 getPartnerName: function () { 1221 // Target the name span specifically â NOT the .message-content-header-contact 1222 // container, which also holds the action menu ("Edit User Notes", "Block 1223 // user", etc.). display-name is what's shown; user-name is the @handle. 1224 var root = document.querySelector('.message-content-header-contact'); 1225 var el = root && (root.querySelector('.display-name') || root.querySelector('.user-name')); 1226 var name = el ? el.textContent.trim() : ''; 1227 if (!name) { 1228 // Fallback: the selected conversation-list row's contact name. 1229 var row = document.querySelector('.message-list a.selected .message-contact'); 1230 name = row ? row.textContent.trim() : ''; 1231 } 1232 // Collapse any stray whitespace/newlines. 1233 name = name.replace(/\\s+/g, ' ').trim(); 1234 if (name) return name; 1235 // Falling back to conv_<id> names the Matrix room after a number. Capture 1236 // the header DOM so the right selector can be added. 1237 var hdr = document.querySelector('.message-content-header-contact'); 1238 Diag.finding('partner-name-fallback', { 1239 conv: Scraper.getConversationId(), 1240 headerFound: !!hdr, 1241 headerLeaves: hdr ? leafTexts(hdr) : null, 1242 headerHtml: hdr ? rowHtml(hdr, 600) : '', 1243 }, 3); 1244 return 'conv_' + Scraper.getConversationId(); 1245 }, 1246 getMessageScrollEl: function () { 1247 return document.querySelector('.message-content-list'); 1248 }, 1249 // Quote replies embed the quoted ORIGINAL as a nested 1250 // app-group-message.message-reply â anything inside it (text, images) 1251 // belongs to the original message and must never be re-captured. NB: the 1252 // class must be .message-reply, NOT .message-reply-wrapper â Fansly puts 1253 // the wrapper class on the container holding the reply's OWN text too. 1254 QUOTE_WRAPPER: '.message-reply', 1255 _messageText: function (el) { 1256 // Take only this message's OWN .message-text nodes â innerText of the 1257 // whole bubble would also capture the quoted original (duplicating it) 1258 // and the quote footer ("Burgeman, 1m"). 1259 var parts = []; 1260 el.querySelectorAll('.message-text').forEach(function (t) { 1261 if (t.closest(Scraper.QUOTE_WRAPPER)) return; // quoted original 1262 if (t.closest('app-group-message') !== el) return; // any other nesting 1263 var s = (t.innerText || '').trim(); 1264 if (s) parts.push(s); 1265 }); 1266 if (parts.length) return parts.join('\\n'); 1267 // Fallback for message types without .message-text (tips, GIFs, â¦): 1268 // innerText minus the quote block and UI chrome.
1269 try { 1270 var clone = el.cloneNode(true); 1271 clone.querySelectorAll( 1272 Scraper.QUOTE_WRAPPER + ', .actions, .tap-backs, .reply-footer' 1273 ).forEach(function (n) { n.remove(); }); 1274 return (clone.innerText || clone.textContent || '').trim(); 1275 } catch (e) { return (el.innerText || '').trim(); } 1276 }, 1277 _collectionTime: function (el) { 1278 var coll = el.closest('app-group-message-collection'); 1279 var t = coll && coll.querySelector('.timestamp .margin-right-text'); 1280 return t ? t.textContent.trim() : ''; 1281 }, 1282 extractMessages: function () { 1283 var scroll = Scraper.getMessageScrollEl(); 1284 if (!scroll) return []; 1285 var convId = Scraper.getConversationId(); 1286 var partner = Scraper.getPartnerName(); 1287 var nodes = scroll.querySelectorAll('app-group-message'); 1288 var out = []; 1289 nodes.forEach(function (el) { 1290 // The quoted original inside a quote reply is itself an 1291 // app-group-message â skip it or it re-delivers as a new message. 1292 if (el.classList.contains('message-reply') || el.closest(Scraper.QUOTE_WRAPPER)) return; 1293 var text = Scraper._messageText(el); 1294 var images = collectMessageImages(el); 1295 if (!text && !images.length) return; 1296 var outgoing = el.classList.contains('my-message'); 1297 var time = Scraper._collectionTime(el); 1298 var domId = el.getAttribute('data-id') || el.id || ''; 1299 // Dedupe on content only â Fansly's timestamp is RELATIVE ("now"/"5s"/ 1300 // "6m") and ticks on every rescan, so including it re-sent every message. 1301 // Media-only messages key on the stable image paths instead. 1302 var content = text || images.map(function (i) { return i.path; }).join(','); 1303 var key = 'f:' + convId + ':' + (domId || hash((outgoing ? '1' : '0') + '|' + content)); 1304 out.push({ 1305 chatWith: partner, 1306 convId: convId, 1307 sender: outgoing ? CONFIG.SELF_LABEL : partner, 1308 text: text, 1309 time: time, 1310 key: key, 1311 images: images, 1312 el: el, 1313 }); 1314 }); 1315 return out; 1316 }, 1317 isMessageReady: function (m) { 1318 // Fansly lazy-loads message media: the bubble renders first, the real 1319 // image URL arrives later. A message is "ready" once every non-avatar, 1320 // non-emoji <img> has a real (non-data:) src, and no attachment shell 1321 // is still waiting to create its <img> at all. 1322 var el = m.el; 1323 if (!el) return true; 1324 var pending = false; 1325 var count = 0; 1326 el.querySelectorAll('img').forEach(function (img) { 1327 // Quote-block images belong to the quoted original â never gate on them. 1328 if (img.closest('[class*="avatar" i], [class*="emoji" i]')) return; 1329 if (img.closest(Scraper.QUOTE_WRAPPER)) return; 1330 count++; 1331 var src = img.currentSrc || img.src || ''; 1332 if (!src || src.indexOf('data:') === 0) pending = true; 1333 }); 1334 if (pending) return false; 1335 if (!count && !Scraper._messageText(el) && 1336 el.querySelector('app-media-preview, [class*="attachment" i]')) { 1337 return false; 1338 } 1339 return true; 1340 }, 1341 listConversations: function () { 1342 var links = document.querySelectorAll('.message-list a[href*="/messages/"]'); 1343 return Array.prototype.map.call(links, function (a) { 1344 var m = (a.getAttribute('href') || '').match(/\\/messages\\/(\\d+)/); 1345 return { 1346 el: a, 1347 id: m ? m[1] : (a.getAttribute('href') || ''), 1348 selected: a.classList.contains('selected'), 1349 unread: Scraper._isUnread(a), 1350 // Name + signature come from the LIST row only â reading the list 1351 // never marks anything read, unlike opening the thread. 1352 name: Scraper._rowName(a), 1353 sig: rowSignature(a), 1354 }; 1355 }); 1356 }, 1357 _rowName: function (a) { 1358 var sels = ['.display-name', '.user-name', '.message-contact']; 1359 for (var i = 0; i < sels.length; i++) { 1360 var el = null; 1361 try { el = a.querySelector(sels[i]); } catch (e) { continue; } 1362 var name = (el ? el.textContent : '').replace(/\\s+/g, ' ').trim(); 1363 if (name) { 1364 Diag.once('rowname:' + sels[i], 'list-row name selector works', { selector: sels[i] }); 1365 return name; 1366 } 1367 } 1368 // No name from the list row â the room would be created as "conv_<id>". 1369 // Capture the row's leaf text so the right selector can be added. 1370 Diag.finding('row-name-missing', { 1371 href: a.getAttribute('href') || '', leaves: leafTexts(a), html: rowHtml(a), 1372 }, 3); 1373 return ''; 1374 }, 1375 // Best-guess until confirmed. Fansly tends to mark unread rows with a 1376 // badge/dot element or an 'unread'/'unseen' class. Tried one at a time so 1377 // the log says WHICH guess fired (and on what element) rather than just 1378 // "unread: true" â that's what pins the real selector down. 1379 _UNREAD_SELECTORS: [ 1380 '[class*="unread" i]', '[class*="unseen" i]', '[class*="new-message" i]', 1381 '[class*="badge" i]', '[class*="notification" i]', 1382 ], 1383 _isUnread: function (a) { 1384 for (var i = 0; i < Scraper._UNREAD_SELECTORS.length; i++) { 1385 var sel = Scraper._UNREAD_SELECTORS[i]; 1386 var hit = null; 1387 try { hit = a.querySelector(sel); } catch (e) { continue; } 1388 if (!hit) continue; 1389 Diag.once('unreadsel:' + sel, 'unread marker matched', { 1390 selector: sel, 1391 onClass: (hit.getAttribute('class') || '').slice(0, 80), 1392 text: (hit.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 24), 1393 }); 1394 Diag.finding('unread-selector-hits', { 1395 selector: sel, onClass: (hit.getAttribute('class') || '').slice(0, 80), 1396 }, 5); 1397 return true; 1398 } 1399 return false;
1400 }, 1401 selectConversation: async function (conv) { 1402 conv.el.click(); 1403 await waitFor(function () { return Scraper.getConversationId() === conv.id; }, 6000, 200); 1404 // give the message list a moment to render 1405 await waitFor(function () { 1406 var s = Scraper.getMessageScrollEl(); 1407 return s && s.querySelector('app-group-message'); 1408 }, 6000, 200); 1409 }, 1410 backfill: async function (scroll) { 1411 var last = -1, stable = 0; 1412 for (var i = 0; i < 100 && stable < 3; i++) { 1413 scroll.scrollTop = 0; 1414 await sleep(650); 1415 var h = scroll.scrollHeight; 1416 if (h === last) stable++; else { stable = 0; last = h; } 1417 } 1418 scroll.scrollTop = scroll.scrollHeight; // return to newest 1419 await sleep(300); 1420 }, 1421 // ---- outbound (Matrix reply -> Fansly) ---- 1422 openConversation: async function (convId) { 1423 convId = String(convId); 1424 if (Scraper.getConversationId() === convId) return true; 1425 var links = document.querySelectorAll('.message-list a[href*="/messages/"]'); 1426 var target = null; 1427 Array.prototype.forEach.call(links, function (a) { 1428 var m = (a.getAttribute('href') || '').match(/\\/messages\\/(\\d+)/); 1429 if (m && m[1] === convId) target = a; 1430 }); 1431 if (!target) return false; // not in the loaded list 1432 target.click(); 1433 return await waitFor(function () { 1434 return Scraper.getConversationId() === convId && 1435 !!document.querySelector('textarea.message-input'); 1436 }, 7000, 200); 1437 }, 1438 sendMessage: async function (text, opts) { 1439 var editor = document.querySelector('app-group-message-input textarea.message-input, textarea.message-input'); 1440 if (!editor) return false; 1441 editor.focus(); 1442 // Angular reactive-form textarea: set via the native value setter, then 1443 // fire 'input' so the form control model updates and send is allowed. 1444 try { 1445 var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; 1446 setter.call(editor, text); 1447 } catch (e) { editor.value = text; } 1448 editor.dispatchEvent(new Event('input', { bubbles: true })); 1449 await sleep(250); 1450 if ((editor.value || '').indexOf(text.slice(0, Math.min(24, text.length))) === -1) { 1451 log('outbound: textarea did not accept text'); 1452 return false; 1453 } 1454 // Per-message stage-only: leave the text in the composer, don't submit. 1455 if (opts && opts.stageOnly) return 'staged'; 1456 if (CONFIG.OUTBOUND_DRYRUN) return 'dryrun'; 1457 // Prefer an explicit send button in the composer; else press Enter. 1458 var btn = document.querySelector( 1459 'app-group-message-input [class*="send" i], app-group-message-input .fa-paper-plane' 1460 ); 1461 if (btn) { 1462 var clickable = btn.closest('button, [role="button"], a, .icon-wrapper') || btn; 1463 clickable.click(); 1464 } else { 1465 ['keydown', 'keypress', 'keyup'].forEach(function (t) { 1466 editor.dispatchEvent(new KeyboardEvent(t, { 1467 key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, 1468 })); 1469 }); 1470 } 1471 // Success signal: Fansly clears the textarea once the message is sent. 1472 var cleared = await waitFor(function () { return (editor.value || '') === ''; }, 3000, 150); 1473 return cleared; 1474 }, 1475 debugConversations: function (convs) { 1476 // One-time-ish dump so we can confirm the real unread marker. Prints only 1477 // structural class names â no message text, no names. 1478 if (Scraper._dumped) return; 1479 Scraper._dumped = true; 1480 log('conversation rows:', convs.length); 1481 convs.slice(0, 12).forEach(function (c, i) { 1482 var group = c.el.querySelector('app-messaging-group') || c.el; 1483 var classes = new Set(); 1484 group.querySelectorAll('*').forEach(function (n) { 1485 (n.getAttribute('class') || '').split(/\\s+/).forEach(function (t) { if (t) classes.add(t); }); 1486 }); 1487 log('row ' + i, { id: c.id, selected: c.selected, unreadGuess: c.unread, 1488 rowClass: c.el.getAttribute('class') || '', classes: Array.from(classes) }); 1489 }); 1490 log('^ compare an UNREAD row vs a READ row above to find the unread class.'); 1491 }, 1492 }; 1493`,jt=` 1494 var Scraper = { 1495 getConversationId: function () { 1496 var m = location.pathname.match(/chat\\/(\\d+)/); 1497 return m ? m[1] : ''; 1498 }, 1499 _currentItem: function () { 1500 var convId = Scraper.getConversationId(); 1501 return document.querySelector('.b-chats__item.current') || 1502 (convId ? document.getElementById(convId) : null); 1503 }, 1504 getPartnerName: function () { 1505 // Try every DISPLAY-NAME source before any handle-based fallback: fans 1506 // without a custom username get a default 'u<id>' handle, so the avatar 1507 // href must be the LAST resort or rooms end up named "OnlyFans: u1839â¦". 1508 var clean = function (s) { return (s || '').replace(/\\s+/g, ' ').trim(); }; 1509 var scroll = Scraper.getMessageScrollEl(); 1510 // 1) Chat header (present whenever a conversation is open, all layouts). 1511 var hdr = document.querySelector( 1512 '.b-chat__header .g-user-name, .b-chat__header .g-user-realname' 1513 ); 1514 var name = hdr ? clean(hdr.textContent) : ''; 1515 if (name) return name; 1516 // 2) Incoming-message avatar alt â the partner's display name. 1517 if (scroll) { 1518 var img = scroll.querySelector('.b-chat__message:not(.m-from-me) .g-avatar img[alt]'); 1519 name = img ? clean(img.getAttribute('alt')) : ''; 1520 if (name) return name; 1521 } 1522 // 3) The conversation-list row (desktop layout only). 1523 var item = Scraper._currentItem(); 1524 var el = item && item.querySelector('.g-user-name'); 1525 name = el ? clean(el.textContent) : ''; 1526 if (name) return name; 1527 // 4) Avatar profile-link handle â may be a default 'u<id>' username. 1528 if (scroll) { 1529 var a = scroll.querySelector('.b-chat__message:not(.m-from-me) a.g-avatar[href]'); 1530 if (a) { 1531 var handle = (a.getAttribute('href') || '') 1532 .replace(/^https?:\\/\\/[^/]+/, '').replace(/^\\/+|\\/+$/g, ''); 1533 if (handle && !/^(my|chats?)\\b/.test(handle) && handle.indexOf('/') === -1) { 1534 Diag.once('partner-name-handle', 'partner name came from the profile handle', { 1535 handle: handle, 1536 }); 1537 return handle; 1538 } 1539 } 1540 } 1541 var hdr2 = document.querySelector('.b-chat__header'); 1542 Diag.finding('partner-name-fallback', { 1543 conv: Scraper.getConversationId(), 1544 headerFound: !!hdr2, 1545 headerLeaves: hdr2 ? leafTexts(hdr2) : null, 1546 headerHtml: hdr2 ? rowHtml(hdr2, 600) : '', 1547 }, 3); 1548 return 'conv_' + Scraper.getConversationId(); 1549 }, 1550 getMessageScrollEl: function () { 1551 return document.querySelector('.b-chat__messages'); 1552 }, 1553 extractMessages: function () { 1554 var scroll = Scraper.getMessageScrollEl(); 1555 if (!scroll) return []; 1556 var convId = Scraper.getConversationId(); 1557 var partner = Scraper.getPartnerName(); 1558 var nodes = scroll.querySelectorAll('[at-attr="chat_message"]'); 1559 var out = [];
1560 nodes.forEach(function (el) { 1561 var textEl = el.querySelector('[at-attr="message_text"], .b-chat__message__text-holder'); 1562 var text = textEl ? (textEl.innerText || '').trim() : ''; 1563 var images = collectMessageImages(el); 1564 if (!text && !images.length) return; // tip-only messages â skip 1565 var outgoing = el.classList.contains('m-from-me'); 1566 var timeEl = el.querySelector('.b-chat__message__time'); 1567 var time = timeEl ? timeEl.textContent.trim() : ''; 1568 // Content-only key (no volatile time) â keeps dedupe stable across 1569 // rescans. Media-only messages key on the stable image paths. 1570 var content = text || images.map(function (i) { return i.path; }).join(','); 1571 var key = 'o:' + convId + ':' + hash((outgoing ? '1' : '0') + '|' + content); 1572 out.push({ 1573 chatWith: partner, 1574 convId: convId, 1575 sender: outgoing ? CONFIG.SELF_LABEL : partner, 1576 text: text, 1577 time: time, 1578 key: key, 1579 images: images, 1580 el: el, 1581 }); 1582 }); 1583 return out; 1584 }, 1585 listConversations: function () { 1586 var items = document.querySelectorAll('.b-chats__item[id]'); 1587 return Array.prototype.map.call(items, function (el) { 1588 return { 1589 el: el, 1590 id: el.id, 1591 selected: el.classList.contains('current'), 1592 unread: el.classList.contains('m-unread'), 1593 link: el.querySelector('a.b-chats__item__link'), 1594 // From the LIST row only â reading the list never marks a chat read. 1595 name: Scraper._rowName(el), 1596 sig: rowSignature(el), 1597 }; 1598 }); 1599 }, 1600 _rowName: function (el) { 1601 var n = el.querySelector('.g-user-name, .g-user-realname'); 1602 var name = (n ? n.textContent : '').replace(/\\s+/g, ' ').trim(); 1603 if (!name) { 1604 Diag.finding('row-name-missing', { 1605 id: el.id || '', leaves: leafTexts(el), html: rowHtml(el), 1606 }, 3); 1607 } 1608 return name; 1609 }, 1610 selectConversation: async function (conv) { 1611 var target = conv.link || conv.el; 1612 target.click(); 1613 await waitFor(function () { return Scraper.getConversationId() === conv.id; }, 6000, 200); 1614 await waitFor(function () { 1615 var s = Scraper.getMessageScrollEl(); 1616 return s && s.querySelector('[at-attr="chat_message"]'); 1617 }, 6000, 200); 1618 }, 1619 backfill: async function (scroll) { 1620 // Scrolling to the top triggers OnlyFans' infinite-loader (.b-chat__loading-top) 1621 // to fetch older messages. Repeat until the height stops growing. 1622 var last = -1, stable = 0; 1623 for (var i = 0; i < 120 && stable < 3; i++) { 1624 scroll.scrollTop = 0; 1625 await sleep(700); 1626 var h = scroll.scrollHeight; 1627 if (h === last) stable++; else { stable = 0; last = h; } 1628 } 1629 scroll.scrollTop = scroll.scrollHeight; 1630 await sleep(300); 1631 }, 1632 // ---- outbound (Matrix reply -> OnlyFans) ---- 1633 openConversation: async function (convId) { 1634 convId = String(convId); 1635 if (Scraper.getConversationId() === convId) return true; 1636 var item = document.getElementById(convId); 1637 var link = item && item.querySelector('a.b-chats__item__link'); 1638 if (!link) return false; // not in the loaded list 1639 link.click(); 1640 return await waitFor(function () { 1641 return Scraper.getConversationId() === convId && 1642 !!document.querySelector('#make_post_form .tiptap, #make_post_form [contenteditable="true"]'); 1643 }, 7000, 200); 1644 }, 1645 sendMessage: async function (text, opts) { 1646 var editor = document.querySelector('#make_post_form .tiptap, #make_post_form [contenteditable="true"]'); 1647 var btn = document.querySelector('#make_post_form [at-attr="send_btn"], #make_post_form .b-chat__btn-submit'); 1648 if (!editor || !btn) return false; 1649 // TipTap/ProseMirror: execCommand('insertText') fires the beforeinput 1650 // events the editor listens for, so its state updates and send enables. 1651 editor.focus(); 1652 try { document.execCommand('selectAll', false, null); } catch (e) {}
1653 try { document.execCommand('insertText', false, text); } catch (e) {} 1654 await sleep(250); 1655 // Verify the text actually landed before we consider sending. 1656 if ((editor.textContent || '').indexOf(text.slice(0, Math.min(24, text.length))) === -1) { 1657 log('outbound: editor did not accept text'); 1658 return false; 1659 } 1660 // Per-message stage-only: leave the text in the composer, don't submit. 1661 if (opts && opts.stageOnly) return 'staged'; 1662 var ready = await waitFor(function () { return !btn.disabled; }, 3000, 100); 1663 if (!ready) return false; 1664 if (CONFIG.OUTBOUND_DRYRUN) return 'dryrun'; 1665 btn.click(); 1666 await sleep(500); 1667 return true; 1668 }, 1669 }; 1670`,tt={fansly:{scraper:Ht,dryRun:!1,autoOpen:!1,outbound:!0,grants:["GM_xmlhttpRequest","GM_getValue","GM_setValue","unsafeWindow"],connect:["fansly.com"]},onlyfans:{scraper:jt,dryRun:!1,autoOpen:!1,outbound:!0,grants:["GM_xmlhttpRequest","GM_getValue","GM_setValue","unsafeWindow"],connect:["onlyfans.com","onlyfansassets.com"]}};function Yt(o){return o.replace(/bridge-ingest\/?$/,"bridge-outbox/")}function Kt(o){return o.replace(/bridge-ingest\/?$/,"bridge-ingest-media/")}function zt(o){return o.replace(/bridge-ingest\/?$/,"bridge-unread/")}function Xt(o){try{return new URL(o).host}catch{return""}}function Qt({platform:o,label:p,token:c,ingestUrl:f,match:v,chatUrl:y}){const d=tt[o]||tt.fansly,F=Xt(f),x="2.11.0",N=d.grants.map(D=>`// @grant ${D}`).join(` 1671`),C=[F].concat(d.connect||[]).filter(Boolean).map(D=>`// @connect ${D}`).join(` 1672`),P={VERSION:x,PLATFORM:o,INGEST_URL:f,OUTBOX_URL:Yt(f),MEDIA_URL:Kt(f),UNREAD_URL:zt(f),TOKEN:c,DEBUG:!0,VERBOSE:!0,HEARTBEAT_MS:3e5,DIAG_THROTTLE_MS:6e4,DRY_RUN:d.dryRun,AUTO_OPEN:!!d.autoOpen,REPORT_UNREAD:!0,UNREAD_FROM_CHANGES:!0,UNREAD_HEARTBEAT_MS:12e4,BACKFILL:!0,OUTBOUND:!!d.outbound,OUTBOUND_DRYRUN:!1,SELF_LABEL:"me",MEDIA_WAIT_SCANS:10,SCAN_INTERVAL_MS:3e3,FLUSH_INTERVAL_MS:4e3,OUTBOX_POLL_MS:5e3,SPLIT_LINES:!0,LINE_GAP_MIN_MS:2e3,LINE_GAP_MAX_MS:5e3},T=d.dryRun?` console.log('%c[bridge] ${p} is in DIAGNOSTIC mode â capturing + logging only, NOT sending. Send the inspector report to enable delivery.', 'color:#e0a500;font-weight:bold'); 1673`:"";return`// ==UserScript== 1674// @name Shared Chat Bridge â ${p} 1675// @namespace shared.chat.bridge 1676// @version ${x} 1677// @description Mirrors your ${p} chat into Shared (Matrix). Keep the chat tab open. 1678// @author Shared 1679// @match ${v} 1680${C} 1681${N} 1682// @run-at document-idle 1683// @noframes 1684// ==/UserScript== 1685 1686/* eslint-disable */ 1687(function () { 1688 'use strict'; 1689 1690 // -- generated for your account ------------------------------------------ 1691 var CONFIG = ${JSON.stringify(P,null,2)}; 1692 1693${T}${d.scraper} 1694${Wt} 1695})(); 1696`}const Jt={class:"matrix-section"},Zt={class:"section-head"},ea={class:"scrapers"},ta={key:0,class:"status"},aa={key:1,class:"status error"},sa={key:2,class:"platform-list"},na={class:"platform-head"},ra={class:"platform-name"},oa={class:"key-row"},ia={class:"key"},la=["onClick"],da=["onClick"],ca={class:"actions"},ua=["onClick"],pa=["disabled","onClick"],ma={key:1,class:"actions"},ga=["disabled","onClick"],fa={class:"scrapers"},va={key:0,class:"status"},ha={key:0,class:"platform-card"},ya={class:"actions"},ba=["disabled"],wa={key:1,class:"platform-card"},_a={class:"bot-field"},ka={class:"key"},Sa={class:"bot-field"},xa={class:"key"},Ca={class:"bot-field"},Ia={class:"key"},Oa={class:"bot-note"},Ea={class:"actions"},Aa=["disabled"],Na=["disabled"],Ta={class:"modal-content"},Da={class:"modal-actions"},Ra={__name:"MatrixSection",setup(o){const p=nt(),c=u(!0),f=u(!1),v=u(""),y=u([]),d=be({}),F=be({}),x=u(!1),N=u(!0),C=u(!1),P=u(!1),T=be({exists:!1,matrix_user_id:null,homeserver_url:"",access_token:null,room_count:0});function D(m){return m?`${m.slice(0,6)}â¦${m.slice(-4)}`:""}function I(m){F[m]=!F[m]}async function U(m){try{await navigator.clipboard.writeText(m),p.success("Copied.")}catch{}}const $=U;Ye(async()=>{try{const{data:m}=await Oe.getBridgeConfig();v.value=m.ingest_url,y.value=m.platforms}catch{f.value=!0}finally{c.value=!1}try{const{data:m}=await Oe.getBridgeBot();Object.assign(T,m)}catch{}finally{N.value=!1}});async function G(m){if(!(m==="remove"&&!window.confirm("Remove the assistant bot? Its token stops working and it leaves your bridge rooms."))){C.value=!0;try{const{data:l}=await Oe.bridgeBotAction(m);Object.assign(T,l),m==="remove"&&(P.value=!1),p.success(m==="remove"?"Assistant bot removed.":m==="regenerate"?"Token regenerated.":"Assistant bot created.")}catch(l){p.error(l?.response?.data?.error||"Could not update the assistant bot.")}finally{C.value=!1}}}async function w(m,l){d[m.platform]=!0;try{const{data:g}=await Oe.generateBridgeToken(m.platform,l);v.value=g.ingest_url;const B=y.value.findIndex(z=>z.platform===m.platform);B!==-1&&(y.value[B]=g.platform),p.success(l?"Key regenerated.":"Key generated.")}catch(g){p.error(g?.response?.data?.error||"Could not generate key.")}finally{d[m.platform]=!1}}function A(m){return m.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function M(m){const l=Qt({platform:m.platform,label:m.label,token:m.token,ingestUrl:v.value,match:m.match,chatUrl:m.chat_url}),g=window.open("","_blank");if(!g){p.error("Your browser blocked the popup. Allow popups and try again.");return}const B=A(l);g.document.write(`<!doctype html> 1697<html> 1698<head> 1699<meta charset="utf-8" /> 1700<title>${m.label} chat bridge â userscript</title> 1701<style> 1702 body { margin: 0; font-family: system-ui, sans-serif; background: #0f0f14; color: #e8e8ef; } 1703 header { padding: 16px 20px; border-bottom: 1px solid #2a2a35; } 1704 h1 { font-size: 16px; margin: 0 0 4px; } 1705 p { margin: 4px 0; font-size: 13px; color: #b7b7c6; } 1706 a { color: #7aa2ff; } 1707 .bar { display: flex; gap: 8px; padding: 12px 20px; } 1708 button { background: #4f6bed; color: #fff; border: 0; border-radius: 6px; padding: 8px 14px; font-size: 13px; cursor: pointer; } 1709 button.copied { background: #2e9e5b; } 1710 textarea { width: 100%; height: calc(100vh - 190px); box-sizing: border-box; border: 0; border-top: 1px solid #2a2a35; background: #16161f; color: #e8e8ef; font-family: ui-monospace, monospace; font-size: 12px; padding: 16px 20px; resize: none; } 1711</style> 1712</head> 1713<body> 1714<header> 1715 <h1>${m.label} chat bridge userscript</h1> 1716 <p>Your key and domain are already filled in below.</p> 1717 <p>1. Install <a href="https://violentmonkey.github.io/get-it/" target="_blank" rel="noopener">Violentmonkey</a>. 1718 2. Copy this script. 3. In Violentmonkey, create a new script, paste, and save. 1719 4. Keep your ${m.label} chat window open.</p> 1720</header> 1721<div class="bar"> 1722 <button id="copy">Copy script</button> 1723</div> 1724<textarea id="src" readonly spellcheck="false">${B}</textarea> 1725<script> 1726 var btn = document.getElementById('copy'); 1727 btn.addEventListener('click', function () { 1728 var ta = document.getElementById('src'); 1729 ta.select(); 1730 ta.setSelectionRange(0, ta.value.length); 1731 try { 1732 navigator.clipboard.writeText(ta.value); 1733 } catch (e) { 1734 document.execCommand('copy'); 1735 } 1736 btn.textContent = 'Copied!'; 1737 btn.className = 'copied'; 1738 setTimeout(function () { btn.textContent = 'Copy script'; btn.className = ''; }, 1800); 1739 }); 1740<\/script> 1741</body> 1742</html>`),g.document.close()}return(m,l)=>(n(),r("div",Jt,[e("div",Zt,[l[10]||(l[10]=e("h2",{class:"section-title"},"Matrix & Chat",-1)),e("button",{type:"button",class:"info-btn",title:"How the chat bridge works",onClick:l[0]||(l[0]=g=>x.value=!0)}," â How it works ")]),l[21]||(l[21]=e("p",{class:"section-intro"},[L(" Read and answer all of your messages from one place. Connect a Matrix app, and optionally bridge your "),e("strong",null,"Fansly"),L(" and "),e("strong",null,"OnlyFans"),L(" DMs so they land in the same inbox. ")],-1)),me(qt),e("div",ea,[l[11]||(l[11]=e("h3",{class:"scrapers-title"},"Chat scrapers",-1)),l[12]||(l[12]=e("p",{class:"scrapers-intro"},[L(" Generate a key, install the userscript in "),e("a",{href:"https://violentmonkey.github.io/get-it/",target:"_blank",rel:"noopener"},"Violentmonkey"),L(", then keep the platform's chat window open in your browser â new messages are mirrored into a Matrix room automatically. ")],-1)),c.value?(n(),r("div",ta,"Loadingâ¦")):f.value?(n(),r("div",aa," Could not load bridge settings. Reload to try again. ")):(n(),r("div",sa,[(n(!0),r(K,null,ge(y.value,g=>(n(),r("div",{key:g.platform,class:"platform-card"},[e("div",na,[e("span",ra,i(g.label),1),e("span",{class:te(["badge",g.token?"badge-on":"badge-off"])},i(g.token?"Key active":"No key"),3)]),g.token?(n(),r(K,{key:0},[e("div",oa,[e("code",ia,i(F[g.platform]?g.token:D(g.token)),1),e("button",{type:"button",class:"btn-ghost",onClick:B=>I(g.platform)},i(F[g.platform]?"Hide":"Reveal"),9,la),e("button",{type:"button",class:"btn-ghost",onClick:B=>E($)(g.token)},"Copy",8,da)]),e("div",ca,[e("button",{type:"button",class:"btn-primary",onClick:B=>M(g)}," Open userscript ",8,ua),e("button",{type:"button",class:"btn-secondary",disabled:d[g.platform],onClick:B=>w(g,!0)},i(d[g.platform]?"Workingâ¦":"Regenerate key"),9,pa)])],64)):(n(),r("div",ma,[e("button",{type:"button",class:"btn-primary",disabled:d[g.platform],onClick:B=>w(g,!1)},i(d[g.platform]?"Generatingâ¦":"Generate key"),9,ga)]))]))),128))]))]),e("div",fa,[l[18]||(l[18]=e("h3",{class:"scrapers-title"},"Assistant bot",-1)),l[19]||(l[19]=e("p",{class:"scrapers-intro"},[L(" Create a Matrix identity for an external assistant/analysis bot. It joins all your private chats â direct messages and bridge rooms â so it can read conversations and post suggestions, and it's added to new ones automatically. Its messages stay in Matrix; only your "),e("code",null,">>"),L("-prefixed replies are sent out to a platform. ")],-1)),N.value?(n(),r("div",va,"Loadingâ¦")):(n(),r(K,{key:1},[T.exists?(n(),r("div",wa,[e("div",_a,[l[13]||(l[13]=e("span",{class:"bot-label"},"Homeserver",-1)),e("code",ka,i(T.homeserver_url),1),e("button",{type:"button",class:"btn-ghost",onClick:l[2]||(l[2]=g=>U(T.homeserver_url))},"Copy")]),e("div",Sa,[l[14]||(l[14]=e("span",{class:"bot-label"},"Bot user ID",-1)),e("code",xa,i(T.matrix_user_id),1),e("button",{type:"button",class:"btn-ghost",onClick:l[3]||(l[3]=g=>U(T.matrix_user_id))},"Copy")]),e("div",Ca,[l[15]||(l[15]=e("span",{class:"bot-label"},"Access token",-1)),e("code",Ia,i(P.value?T.access_token:D(T.access_token)),1),e("button",{type:"button",class:"btn-ghost",onClick:l[4]||(l[4]=g=>P.value=!P.value)},i(P.value?"Hide":"Reveal"),1),e("button",{type:"button",class:"btn-ghost",onClick:l[5]||(l[5]=g=>U(T.access_token))},"Copy")]),e("p",Oa,[l[16]||(l[16]=L(" In ",-1)),e("strong",null,i(T.room_count),1),l[17]||(l[17]=L(" private chat(s). Treat the token like a password â anyone with it can post as this bot. ",-1))]),e("div",Ea,[e("button",{type:"button",class:"btn-secondary",disabled:C.value,onClick:l[6]||(l[6]=g=>G("regenerate"))},i(C.value?"Workingâ¦":"Regenerate token"),9,Aa),e("button",{type:"button",class:"btn-ghost danger",disabled:C.value,onClick:l[7]||(l[7]=g=>
1742G("remove"))}," Remove bot ",8,Na)])])):(n(),r("div",ha,[e("div",ya,[e("button",{type:"button",class:"btn-primary",disabled:C.value,onClick:l[1]||(l[1]=g=>G("create"))},i(C.value?"Creatingâ¦":"Create assistant bot"),9,ba)])]))],64))]),x.value?(n(),r("div",{key:0,class:"modal-overlay",onClick:l[9]||(l[9]=ce(g=>x.value=!1,["self"]))},[e("div",Ta,[l[20]||(l[20]=Et('<h3 class="modal-title" data-v-1eb56104>How the chat bridge works</h3><div class="modal-body" data-v-1eb56104><ol data-v-1eb56104><li data-v-1eb56104><strong data-v-1eb56104>Install Violentmonkey.</strong> It's a free browser extension that runs small scripts on specific sites. Get it from <a href="https://violentmonkey.github.io/get-it/" target="_blank" rel="noopener" data-v-1eb56104> violentmonkey.github.io</a> (Chrome, Edge, Firefox). </li><li data-v-1eb56104><strong data-v-1eb56104>Generate a key</strong> for the platform you want to bridge, then click <em data-v-1eb56104>Open userscript</em>. A window opens with the script already filled in with your key and the correct domain. </li><li data-v-1eb56104><strong data-v-1eb56104>Add it to Violentmonkey.</strong> Copy the script, open the Violentmonkey dashboard, create a new script, paste, and save. </li><li data-v-1eb56104><strong data-v-1eb56104>Keep the chat window open.</strong> The script only runs while the platform's chat page is open in your browser. Each conversation gets its own Matrix room here. If you close the tab, mirroring pauses until you open it again. </li><li data-v-1eb56104><strong data-v-1eb56104>Unread stays unread.</strong> The script never opens a conversation by itself, so nothing is marked as read behind your back. When a thread has unread mail you get a <code data-v-1eb56104>ð Unread message</code> notice in its Matrix room â no message text, nothing read yet. Send <code data-v-1eb56104>/read</code> in that room when you want it opened and synced across; the messages then appear as normal. <code data-v-1eb56104>/refresh</code> (in any bridge room) reloads the platform tab if it ever gets stuck. </li><li data-v-1eb56104><strong data-v-1eb56104>Reply from Matrix.</strong> Your bridged conversations appear alongside your Shared chats. To send a reply back out to the platform, start your message with <code data-v-1eb56104>>></code> â e.g. <code data-v-1eb56104>>> thanks, talk soon!</code>. Messages without the <code data-v-1eb56104>>></code> prefix stay in Matrix only, so notes and assistant bots in the room are never sent out. The reply is typed into the platform's chat and sent, so keep that tab open too. </li></ol><p class="modal-note" data-v-1eb56104> Your key authenticates the script â treat it like a password. If it ever leaks, hit <em data-v-1eb56104>Regenerate key</em> and update the script. </p><p class="modal-note" data-v-1eb56104> Something not working? The script keeps a diagnostic record as things happen. Open the browser console on the platform's tab and run <code data-v-1eb56104>__bridge.report()</code> â it prints what it has seen (and what failed), including notes kept from earlier sessions. </p></div>',2)),e("div",Da,[e("button",{type:"button",class:"btn-primary",onClick:l[8]||(l[8]=g=>x.value=!1)},"Got it")])])])):b("",!0)]))}},Ua=Ae(Ra,[["__scopeId","data-v-1eb56104"]]),We="0x14a34",ye=84532,ve=u(null);let at=!1;const Ma=new Proxy({},{get(o,p,c){const f=ve.value;if(!f)throw new Error("x402 wallet is not connected");const v=Reflect.get(f,p,c);return typeof v=="function"?v.bind(f):v},has(o,p){const c=ve.value;return c?p in c:!1},ownKeys(o){const p=ve.value;return p?Reflect.ownKeys(p):[]},getOwnPropertyDescriptor(o,p){const c=ve.value;if(c)return Object.getOwnPropertyDescriptor(c,p)}});function Fa(){at||(Vt(fe,Ma),at=!0)}async function He(o,p=!1){if(!o)return"";if(!p&&o.selectedAddress)return o.selectedAddress;const c=await o.request({method:p?"eth_requestAccounts":"eth_accounts"});return Array.isArray(c)&&c.length>0?c[0]:""}const Pa=At("x402Wallet",()=>{const o=u(""),p=u(!1),c=u(null),f=u(!1),v=u(""),y=typeof window<"u",d=q(()=>y&&window.ethereum||null),F=q(()=>d.value!==null),x=q(()=>c.value===ye),N=q(()=>"Base Sepolia"),C=q(()=>p.value&&x.value);function P(){o.value="",p.value=!1,c.value=null,ve.value=null}async function T(w){try{return await w.request({method:"wallet_switchEthereumChain",params:[{chainId:We}]}),c.value=ye,!0}catch(A){if(A.code===4902||A?.message?.includes("Unrecognized chain ID"))try{return await w.request({method:"wallet_addEthereumChain",params:[{chainId:We,chainName:"Base Sepolia",rpcUrls:["https://sepolia.base.org"],nativeCurrency:{name:"Ether",symbol
1742:"ETH",decimals:18},blockExplorerUrls:["https://sepolia.basescan.org"]}]}),c.value=ye,!0}catch(M){return v.value=M?.message||"Failed to add Base network to wallet",!1}return v.value=A?.message||"Wallet must be connected to the Base network",!1}}async function D(w,A){const M=A||await He(w,!0);if(!M)throw new Error("Wallet did not return an account");const m=Nt({chain:Dt,transport:Tt(w)}),l=new Proxy(m,{get(g,B,z){if(B==="account"){const V=w?.selectedAddress||o.value||M||M?.toString?.();return V?{address:V}:void 0}return B==="signTypedData"?async V=>{const J=w?.selectedAddress||o.value||M||await He(w,!0);if(!J)throw new Error("Unable to determine active wallet account");return g.signTypedData({...V,account:J})}:Reflect.get(g,B,z)}});ve.value=l,Fa()}async function I(){if(!F.value)return v.value="No compatible Ethereum wallet detected. Please install Coinbase Wallet or MetaMask.",!1;f.value=!0,v.value="";const w=d.value;try{const A=await w.request({method:"eth_chainId"});if(c.value=parseInt(A,16),A.toLowerCase()!==We&&!await T(w))return f.value=!1,!1;const M=await He(w,!0);if(!M)throw new Error("Wallet did not return any accounts");return o.value=M,p.value=!0,c.value=ye,await D(w,M),!0}catch(A){return v.value=A?.message||"Failed to connect wallet",P(),!1}finally{f.value=!1}}function U(){P(),v.value=""}async function $(w){if(!w||w.length===0){P();return}const A=d.value,M=w[0];if(o.value=M,p.value=!0,!A){v.value="Wallet provider unavailable";return}await D(A,M)}async function G(w){const A=w.startsWith("0x")?parseInt(w,16):Number(w);if(c.value=A,A!==ye){v.value=`Please switch your wallet to the ${N.value} network to continue.`;return}v.value="",p.value&&d.value&&o.value&&await D(d.value,o.value)}if(y){const w=d.value;w&&(w.removeListener?.("accountsChanged",$),w.on?.("accountsChanged",$),w.removeListener?.("chainChanged",G),w.on?.("chainChanged",G))}return{address:o,chainId:c,connected:p,loading:f,error:v,isSupported:F,isCorrectNetwork:x,isReady:C,networkName:N,connect:I,disconnect:U}});function La(o,p=null){const c=parseFloat(o)||0,f=p?.fee_percent??.02,v=p?.fee_fixed??.1,y=c*f+v,d=c+y;return{baseAmount:c,feeAmount:y,totalAmount:d}}function $a(o){return parseFloat(o).toFixed(2).replace(/\.?0+$/,"")}const Ba={class:"modal-content"},Ga={class:"modal-body"},qa={key:0,class:"input-step"},Va={class:"amount-input-group"},Wa={key:0,class:"error-message"},Ha={key:1,class:"fee-breakdown"},ja={class:"fee-row"},Ya={class:"fee-value"},Ka={class:"fee-row"},za={class:"fee-label"},Xa={class:"fee-value"},Qa={class:"fee-row fee-total"},Ja={class:"fee-value"},Za={key:2,class:"fee-explanation"},es={class:"explanation-text"},ts={class:"wallet-section"},as={key:0},ss={key:1},ns={key:2},rs=["disabled"],os={key:0,class:"loading-spinner"},is={key:1},ls={key:2},ds={key:3},cs={key:0,class:"error-message"},us={key:1,class:"error-message"},ps=["disabled"],ms={key:0,class:"loading-spinner"},gs={key:1,class:"processing-step"},fs={class:"processing-amount"},vs={class:"processing-description"},hs={key:2,class:"success-step"},ys={key:0,class:"status-message success"},bs={key:1,class:"success-details"},ws={key:3,class:"error-step"},_s={__name:"AddFundsModal",props:{show:{type:Boolean,default:!1}},emits:["close","credit-updated"],setup(o,{emit:p}){const c=o,f=p,v=Pa(),y=rt(),d=ot(),{address:F,connected:x,isReady:N,isSupported:C,loading:P,error:T}=Rt(v),D=u("input"),I=u(""),U=u(!1),$=u(""),G=u(""),w=u(""),A=u(""),M=q(()=>{const O=parseFloat(I.value);return I.value?isNaN(O)?"Please enter a valid number":O<.01?"Minimum amount is $0.01":O>1e4?"Maximum amount is $10,000.00":"":""}),m=q(()=>!M.value&&parseFloat(I.value)>=.01),l=q(()=>{if(!m.value)return null;const O=d.billingInfo?.fee_config||null;return La(I.value,O)});function g(O){return $a(O)}const B=q(()=>F.value?`${F.value.slice(0,6)}...${F.value.slice(-4)}`:""),z=q(()=>T.value);function V(){$.value="",G.value=""}async function J(){await v.connect()}async function ae(){if(m.value){if(!C.value){T.value="No compatible wallet detected. Please install Coinbase Wallet or MetaMask.";return}if(!(!N.value&&!await v.connect())){U.value=!0,G.value="info",$.value="Waiting for wallet confirmation...",D.value="processing";try{const O=await je.createCryptoPayment(I.value,y.user?.id||y.user?.pk),_=O.headers?.["x-payment-response"];_&&($.value=_),A.value=O.data.transaction_id,$.value=O.data.message||"Payment c
1742onfirmed!",G.value="success",D.value="success",f("credit-updated")}catch(O){console.error("Error creating payment:",O),O.response?.data&&console.error("x402 response payload:",O.response.data);let _=O.response?.data?.error||O?.message||"Failed to create payment";!O.response&&(O?.code===4001||/user rejected/i.test(_))&&(_="Transaction was rejected in your wallet. Please try again."),D.value="error",w.value=_}finally{U.value=!1}}}}function ie(){D.value="input",I.value="",$.value="",G.value="",w.value="",A.value=""}function Z(){ie(),f("close")}return Ee(()=>c.show,O=>{O||ie()}),(O,_)=>o.show?(n(),r("div",{key:0,class:"modal-overlay",onClick:ce(Z,["self"])},[e("div",Ba,[e("div",{class:"modal-header"},[_[1]||(_[1]=e("h2",null,"Add Credit",-1)),e("button",{onClick:Z,class:"close-button"},"Ã")]),e("div",Ga,[D.value==="input"?(n(),r("div",qa,[_[5]||(_[5]=e("p",{class:"description"}," Add credit to your account using cryptocurrency. Minimum amount: $0.01 ",-1)),e("div",Va,[_[4]||(_[4]=e("label",{for:"amount"},"Credit Amount ($):",-1)),R(e("input",{id:"amount","onUpdate:modelValue":_[0]||(_[0]=he=>I.value=he),type:"number",min:"0.01",max:"10000",step:"0.01",placeholder:"0.01",class:"amount-input",onInput:V},null,544),[[j,I.value]]),M.value?(n(),r("p",Wa,i(M.value),1)):b("",!0),m.value&&l.value?(n(),r("div",Ha,[e("div",ja,[_[2]||(_[2]=e("span",{class:"fee-label"},"Credit Amount:",-1)),e("span",Ya,"$"+i(g(l.value.baseAmount)),1)]),e("div",Ka,[e("span",za," Processing Fee ("+i(((E(d).billingInfo?.fee_config?.fee_percent||.02)*100).toFixed(0))+"% + $"+i(g(E(d).billingInfo?.fee_config?.fee_fixed||.1))+"): ",1),e("span",Xa,"$"+i(g(l.value.feeAmount)),1)]),e("div",Qa,[_[3]||(_[3]=e("span",{class:"fee-label"},"Total to Pay:",-1)),e("span",Ja,"$"+i(g(l.value.totalAmount)),1)])])):b("",!0),m.value&&l.value?(n(),r("div",Za,[e("p",es," In order for us to guarantee the creator received 100% of what you spend on them, we need to cover costs of both receiving and sending money. We have kept it as low as possible. "+i(((E(d).billingInfo?.fee_config?.fee_percent||.02)*100).toFixed(0))+"% plus "+i((E(d).billingInfo?.fee_config?.fee_fixed||.1)*100)+"c per transaction. The creator receives 100% of what they withdraw. Their fees are covered by this too! ",1)])):b("",!0)]),e("div",ts,[e("div",{class:te(["wallet-status",{connected:E(N)}])},[E(N)?(n(),r("span",as,"Connected wallet: "+i(B.value),1)):E(x)?(n(),r("span",ss,"Please switch your wallet to the Base network to continue.")):(n(),r("span",ns,"Connect a Base-compatible wallet (Coinbase Wallet or MetaMask) to continue."))],2),e("button",{onClick:J,disabled:E(P),class:"btn btn-secondary wallet-button"},[E(P)?(n(),r("span",os)):b("",!0),E(N)?(n(),r("span",is,"Wallet Connected")):E(x)?(n(),r("span",ls,"Switch Network")):(n(),r("span",ds,"Connect Wallet"))],8,rs),z.value?(n(),r("p",cs,i(z.value),1)):E(C)?b("",!0):(n(),r("p",us," No compatible wallet detected. Install Coinbase Wallet or MetaMask. "))]),e("button",{onClick:ae,disabled:!m.value||U.value||E(P),class:"btn btn-primary payment-button"},[U.value?(n(),r("span",ms)):b("",!0),!U.value&&l.value?(n(),r(K,{key:1},[L(" Pay $"+i(g(l.value.totalAmount))+" USDC ",1)],64)):U.value?(n(),r(K,{key:3},[L(" Creating Payment... ")],64)):(n(),r(K,{key:2},[L(" Pay with Crypto ")],64))],8,ps)])):D.value==="processing"?(n(),r("div",gs,[_[6]||(_[6]=e("div",{class:"processing-icon"},"â³",-1)),_[7]||(_[7]=e("h3",null,"Confirm in Your Wallet",-1)),e("p",fs,"Total Amount: $"+i(l.value?g(l.value.totalAmount):I.value),1),e("p",vs," Approve the transaction in your connected wallet. Once confirmed on Base, we will automatically credit your account with $"+i(I.value)+". ",1),$.value?(n(),r("div",{key:0,class:te(["status-message",G.value])},i($.value),3)):b("",!0)])):D.value==="success"?(n(),r("div",hs,[_[8]||(_[8]=e("div",{class:"success-icon"},"â",-1)),_[9]||(_[9]=e("h3",null,"Payment Successful!",-1)),e("p",null,"Your account has been credited
1742with $"+i(I.value),1),$.value?(n(),r("p",ys,i($.value),1)):b("",!0),A.value?(n(),r("p",bs,"Transaction ID: "+i(A.value),1)):b("",!0),e("button",{onClick:Z,class:"btn btn-primary"},"Done")])):D.value==="error"?(n(),r("div",ws,[_[10]||(_[10]=e("div",{class:"error-icon"},"â",-1)),_[11]||(_[11]=e("h3",null,"Payment Failed",-1)),e("p",null,i(w.value),1),e("div",{class:"error-actions"},[e("button",{onClick:ie,class:"btn btn-secondary"},"Try Again"),e("button",{onClick:Z,class:"btn btn-outline"},"Cancel")])])):b("",!0)])])])):b("",!0)}},ks=Ae(_s,[["__scopeId","data-v-312cedca"]]),Ss=["aria-live"],xs={class:"error-content"},Cs={key:0,class:"error-icon"},Is={key:0,width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},Os={key:1,width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},Es={class:"error-text"},As={key:0,class:"error-title"},Ns={class:"error-description"},Ts={key:1,class:"error-details"},Ds={key:1,class:"error-actions"},Rs=["disabled","aria-label"],Us={key:0,class:"spinner",width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},Ms={key:0,class:"error-progress"},Fs={__name:"ErrorMessage",props:{show:{type:Boolean,default:!1},type:{type:String,default:"error",validator:o=>["error","warning","info","success"].includes(o)},title:{type:String,default:""},message:{type:String,required:!0},details:{type:String,default:""}
1742,showDetails:{type:Boolean,default:!1},canRetry:{type:Boolean,default:!1},canDismiss:{type:Boolean,default:!0},retryText:{type:String,default:"Retry"},action:{type:String,default:""},showIcon:{type:Boolean,default:!0},showProgress:{type:Boolean,default:!1},progress:{type:Number,default:null,validator:o=>o===null||o>=0&&o<=100},autoHide:{type:Boolean,default:!1},autoHideDelay:{type:Number,default:5e3},isLiveRegion:{type:Boolean,default:!0}},emits:["retry","dismiss","update:show"],setup(o,{emit:p}){const c=o,f=p,v=u(!1),y=u(!navigator.onLine),d=u(null),F=q(()=>{const I=[`error-${c.type}`];return y.value&&I.push("error-offline"),I});function x(){v.value||(v.value=!0,f("retry"),setTimeout(()=>{v.value=!1},1e3))}function N(){f("dismiss"),f("update:show",!1)}function C(){y.value=!1}function P(){y.value=!0}function T(){c.autoHide&&c.show&&(d.value=setTimeout(()=>{f("update:show",!1)},c.autoHideDelay))}function D(){d.value&&(clearTimeout(d.value),d.value=null)}return Ee(()=>c.show,I=>{I?T():D()}),Ye(()=>{window.addEventListener("online",C),window.addEventListener("offline",P),c.show&&c.autoHide&&T()}),st(()=>{window.removeEventListener("online",C),window.removeEventListener("offline",P),D()}),(I,U)=>o.show?(n(),r("div",{key:0,class:te(["error-message",F.value]),role:"alert","aria-live":o.isLiveRegion?"assertive":"polite","aria-atomic":!0},[e("div",xs,[o.showIcon?(n(),r("div",Cs,[y.value?(n(),r("svg",Is,[...U[0]||(U[0]=[e("path",{d:"M21 12a9 9 0 0 1-9 9 9 9 0 0 1-9-9 9 9 0 0 1 9-9c2.4 0 4.6.9 6.3 2.4"},null,-1),e("path",{d:"M21 3v6h-6"},null,-1),e("path",{d:"M3 12a9 9 0 0 0 9 9"},null,-1)])])):(n(),r("svg",Os,[...U[1]||(U[1]=[e("circle",{cx:"12",cy:"12",r:"10"},null,-1),e("path",{d:"m15 9-6 6"},null,-1),e("path",{d:"m9 9 6 6"},null,-1)])]))])):b("",!0),e("div",Es,[o.title?(n(),r("div",As,i(o.title),1)):b("",!0),e("div",Ns,i(o.message),1),o.showDetail
1742s&&o.details?(n(),r("div",Ts,i(o.details),1)):b("",!0)]),o.canRetry||o.canDismiss?(n(),r("div",Ds,[o.canRetry?(n(),r("button",{key:0,onClick:x,disabled:v.value,class:"btn btn-retry btn-touch","aria-label":`Retry ${o.action||"action"}`},[v.value?(n(),r("svg",Us,[...U[2]||(U[2]=[e("path",{d:"M21 12a9 9 0 0 1-9 9 9 9 0 0 1-9-9 9 9 0 0 1 9-9c2.4 0 4.6.9 6.3 2.4"},null,-1)])])):b("",!0),L(" "+i(v.value?"Retrying...":o.retryText),1)],8,Rs)):b("",!0),o.canDismiss?(n(),r("button",{key:1,onClick:N,class:"btn btn-dismiss btn-touch","aria-label":"Dismiss error message"},[...U[3]||(U[3]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("path",{d:"m18 6-12 12"}),e("path",{d:"m6 6 12 12"})],-1)])])):b("",!0)])):b("",!0)]),o.showProgress&&o.progress!==null?(n(),r("div",Ms,[e("div",{class:"progress-bar",style:Ut({width:`${o.progress}%`})},null,4)])):b("",!0)],10,Ss)):b("",!0)}},Ps=Ae(Fs,[["__scopeId","data-v-b833b666"]]),S={NETWORK:"network",AUTHENTICATION:"authentication",AUTHORIZATION:"authorization",VALIDATION:"validation",SERVER:"server",CLIENT:"client",OFFLINE:"offline"},oe={[S.NETWORK]:"Network connection failed. Please check your internet connection.",[S.AUTHENTICATION]:"Your session has expired. Please log in again.",[S.AUTHORIZATION]:"You don't have permission to perform this action.",[S.VALIDATION]:"Please check your input and try again.",[S.SERVER]:"Server error occurred. Please try again later.",[S.CLIENT]:"An unexpected error occurred.",[S.OFFLINE]:"You appear to be offline. Please check your connection."};function Ls(o,p={}){const{fallbackMessage:c=oe[S.CLIENT],includeDetails:f=!1,action:v=""}=p;let y=S.CLIENT,d=c,F="",x=!1,N=null;if(!o)return{type:y,message:d,details:F,canRetry:x,statusCode:N};if(o.response){N=o.response.status;const C=o.response.data;N===401?(y=S.AUTHENTICATION,d=oe[S.AUTHENTICATION]):N===403?(y=S.AUTHORIZATION,d=oe[S.AUTHORIZATION]):N===400?(y=S.VALIDATION,d=$s(C)):N>=500?(y=S.SERVER,d=oe[S.SERVER],x=!0):N>=400&&(y=S.CLIENT,d=C?.message||C?.detail||c),f&&(F=JSON.stringify(C,null,2))}else o.request?(y=S.NETWORK,d=oe[S.NETWORK],x=!0,navigator.onLine||(y=S.OFFLINE,d=oe[S.OFFLINE])):(d=o.message||c,f&&(F=o.stack||""));return{type:y,message:d,details:F,canRetry:x,statusCode:N,action:v}}function $s(o){if(!o)return oe[S.VALIDATION];if(typeof o=="object"){const p=[];o.non_field_errors&&p.push(...o.non_field_errors);for(const[c,f]of Object.entries(o))c!=="non_field_errors"&&Array.isArray(f)&&p.push(...f);if(p.length>0)return p.join(" ");if(o.message)return o.message;if(o.detail)return o.detail}return oe[S.VALIDATION]}function Bs(o,p={}){const c=Ls(o,p);return{show:!0,type:c.type===S.OFFLINE?"warning":"error",title:Gs(c.type),message:c.message,details:c.details,canRetry:c.canRetry,canDismiss:!0,retryText:"Retry",action:c.action,showIcon:!0,showDetail
1742s:p.includeDetails||!1,autoHide:c.type===S.SUCCESS,autoHideDelay:5e3}}function Gs(o){return{[S.NETWORK]:"Connection Error",[S.AUTHENTICATION]:"Authentication Required",[S.AUTHORIZATION]:"Access Denied",[S.VALIDATION]:"Invalid Input",[S.SERVER]:"Server Error",[S.CLIENT]:"Error",[S.OFFLINE]:"Offline"}[o]||"Error"}const qs={class:"settings-page"},Vs={class:"container"},Ws={class:"section-nav","aria-label":"Settings sections"},Hs=["href","onClick"],js={id:"profile",class:"settings-section"},Ys={key:0,class:"section-card"},Ks={class:"profile-picture-container"},zs={class:"avatar-wrapper"},Xs=["src"],Qs={key:1,class:"avatar-placeholder"},Js={class:"avatar-controls"},Zs={class:"section-card"},en={class:"section-card"},tn={class:"section-card"},an={class:"checkbox-label"},sn=["disabled"],nn={class:"section-card"},rn={class:"radio-options"},on={class:"radio-label"},ln={class:"radio-label"},dn={class:"radio-label"},cn={class:"radio-label"},un={class:"section-card"},pn={class:"radio-options"},mn={class:"radio-label"},gn={class:"radio-label"},fn={class:"radio-label"},vn={class:"section-card"},hn={class:"form-stack"},yn={class:"password-buttons"},bn=["disabled"],wn={key:0,id:"creator-settings",class:"settings-section"},_n={class:"section-card"},kn={class:"section-card"},Sn={class:"radio-options"},xn={class:"radio-label"},Cn={class:"radio-label"},In={class:"radio-label"},On={class:"section-card"},En={key:1,id:"viewer-details",class:"settings-section"},An={class:"section-card"},Nn=["min"],Tn=["disabled"],Dn={class:"section-card"},Rn={key:0,class:"loading"},Un={key:1,class:"empty-message"},Mn={key:2,class:"viewers-list"},Fn={class:"viewer-info"},Pn={class:"viewer-details"},Ln={class:"detail-item"},$n={class:"detail-item"},Bn={key:0,class:"detail-item"},Gn={key:0,class:"expired-label"},qn={class:"viewer-actions"},Vn=["onClick"],Wn=["onClick"],Hn={key:2,id:"subscription",class:"settings-section"},jn={class:"section-card"},Yn={class:"checkbox-label"},Kn={key:0,class:"pricing-grid"},zn={class:"pricing-item"},Xn={class:"price-input-group"},Qn={class:"pricing-item"},Jn={class:"price-input-group"},Zn={class:"form-actions"},er=["disabled"],tr={id:"billing",class:"settings-section"},ar={class:"section-card"},sr={class:"credit-balance-display"},nr={class:"credit-amount"},rr={key:0,class:"section-card"},or={class:"wallet-input-group"},ir={key:0,class:"error-text"},lr=["disabled"],dr={key:1,class:"section-card"},cr={class:"billing-header"},ur={key:0,class:"loading"},pr={key:1,class:"error-text"},mr={key:2,class:"billing-content"},gr={class:"billing-cards"},fr={class:"billing-card"},vr={class:"credit-amount-sm"},hr={key:0,class:"credit-warning"},yr={key:1,class:"credit-depleted"},br={class:"billing-card"},wr={class:"usage-amount"},_r={class:"cost-amount"},kr={class:"billing-card"},Sr={class:"usage-amount"},xr={class:"cost-amount"},Cr={class:"billing-card"},Ir={class:"viewer-breakdown"},Or={key:0,class:"empty-message"},Er={key:1,class:"viewer-table"},Ar={key:1},Nr={class:"section-card"},Tr={key:0,class:"loading"},Dr={key:1,class:"empty-message"},Rr={key:2},Ur={class:"transactions-list"},Mr={class:"transaction-main"},Fr={class:"transaction-type"},Pr={class:"transaction-details"},Lr={class:"transaction-parties"},$r={key:0},Br={key:1},Gr={key:0},qr={key:1},Vr={key:0,class:"transaction-description"},Wr={class:"transaction-date"},Hr={key:0,class:"pagination-controls"},jr=["disabled"],Yr={class:"pagination-info"},Kr={key:0},zr=["disabled"],Xr={class:"about-link"},Qr={class:"modal-body"},Jr={key:0,class:"loading"},Zr={key:1,class:"empty-message"},eo={key:2,class:"followers-table"},to={class:"follower-info"},ao=["src","alt"],so={key:1,class:"follower-avatar-placeholder"},no={class:"follower-username"},ro={class:"follower-email"},oo=["href"],io={key:1,class:"follower-email-hidden"},lo={__name:"ProfileView",setup(o){const p=$t(),c=Mt(),f=rt(),v=ot(),y=nt(),d=q(()=>f.user),F=q(()=>f.isViewer),x=q(()=>f.isCreator),N=Bt(),C=u(N.isOnline),P=[{id:"profile",label:"Profile",creatorOnly:!1},{id:"creator-settings",label:"Creator Settings",creatorOnly:!0},{id:"viewer-details",label:"Viewer Details",creatorOnly:!0},{id:"subscription",label:"Subscription",creatorOnly:!0},{id:"billing",label:"Billing and Usage",creatorOnly:!1}],T=q(()=>P.filter(s=>!s.creatorOnly||x.value)),D=u("profile"),I=u(null),U=u(null),$=u(!1),G=u(""),w=u("success"),A=u(0),M=u([]),m=u(!1),l=u(1),g=u(1),B=u(0),z=u(!1),V=u(""),J=u(!1),ae=u(""),ie=u(!1),Z=u(null),O=u(null),_=u([]),he=u(!1),we=u(!1),Ne=u(!1),Te=u([]),De=u(!1),_e=u(!1),le=u(!0),Re=u(!1),h=be({bio:"",email:"",watermark_text:"",avatar:null,nsfw_access_mode:1,notification_preference:"ALL"}),W=be({old_password:"",new_password:"",new_password2:""}),Y=u({viewerUsername:"",accessLevel:"FREE",expiresAt:Ke()}),H=u({subscriptions_enabled:!1,member_price:null,vip_price:null}),ee=u({profile_state:1});function it(){return new Date().toISOString().split("T")[0]}function Ke(){const s=new Date;return s.setDate(s.getDate()+30),s.toISOString().split("T")[0]}function ke(s){return parseFloat(s||0).toFixed(2).replace(/\.?0+$/,"")}function Se(s){const t=parseFloat(s||0);return t>=1e3?`${(t/1e3).toFixed(1)}TB`:t>=1?`${t.toFixed(1)}GB`:`${(t*1e3).toFixed(0)}MB`}function Ue(s){return new Date(s).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}function X(s,t="success"){G.value=s,w.value=t,setTimeout(()=>{G.value=""},5e3)}function se(s,t={}){O.value=Bs(s,{action:t.action||"operation",includeDetails:t.includeDetails||!1})}function ue(){O.value=null}function lt(){ue()}function Me(s){const t=document.getElementById(s);if(!t)return;D.value=s;const a=t.getBoundingClientRect().top+window.scrollY-130;window.scrollTo({top:a,behavior:"smooth"}),history.replaceState(null,"",`#${s}`)}function ze(){I.value&&I.value.disconnect();const t=T.value.map(k=>k.id).map(k=>
1742document.getElementById(k)).filter(Boolean);t.length&&(I.value=new IntersectionObserver(k=>{const a=k.filter(ne=>ne.isIntersecting).sort((ne,Ce)=>Ce.intersectionRatio-ne.intersectionRatio);a[0]&&(D.value=a[0].target.id)},{rootMargin:"-20% 0px -60% 0px",threshold:[0,.25,.5]}),t.forEach(k=>I.value.observe(k)))}async function dt(s){const t=s.target.files[0];if(!t)return;const k=new Image,a=document.createElement("canvas"),ne=a.getContext("2d");k.onload=()=>{a.width=200,a.height=200;const pe=Math.max(200/k.width,200/k.height),It=(200-k.width*pe)/2,Ot=(200-k.height*pe)/2;ne.drawImage(k,It,Ot,k.width*pe,k.height*pe),h.avatar=a.toDataURL("image/png")};const Ce=new FileReader;Ce.onload=pe=>{k.src=pe.target.result},Ce.readAsDataURL(t),await ut(t)}function ct(s,t){const k=d.value?.[s];return String(k??"")===String(t??"")}async function de(s,t){if(!ct(s,t)){if(!C.value){se(new Error("You are currently offline. Please check your connection and try again."),{action:"saving profile"});return}ue();try{await f.updateProfile({[s]:t??""}),X("Saved","success")}catch(k){console.error("Error updating profile:",k),se(k,{action:"saving profile"}),d.value&&s in h&&(h[s]=d.value[s]??(s==="notification_preference"?"ALL":""))}}}async function ut(s){if(!C.value){se(new Error("You are currently offline. Please check your connection and try again."),{action:"saving profile"});return}ue();try{const t=new FormData;t.append("avatar",s),await f.updateProfile(t),X("Profile picture saved","success")}catch(t){console.error("Error updating avatar:",t),se(t,{action:"saving profile"}),h.avatar=d.value?.avatar||null}}async function pt(){if(!C.value){se(new Error("You are currently offline. Please check your connection and try again."),{action:"changing password"});return}if(W.new_password!==W.new_password2){X("New passwords do not match","error");return}if(W.new_password.length<8){X("Password must be at least 8 characters long","error");return}$.value=!0,ue();try{await re.changePassword(W),X("Password changed successfully!","success"),W.old_password="",W.new_password="",W.new_password2=""}catch(s){console.error("Error changing password:",s),se(s,{action:"changing password"})}finally{$.value=!1}}function mt(){f.logout(),p.push("/login")}function gt(s){return s.source_user&&s.source_user.id===d.value?.id}function Fe(s){return s.source_user&&s.source_user.id===d.value?.id?-Math.abs(s.amount):Math.abs(s.amount)}async function Xe(){try{const s=await re.getCreditBalance();A.value=parseFloat(s.data.credit_balance||0)}catch(s){console.error("Error loading credit balance:",s),A.value=0}}async function Pe(s=1){m.value=!0;try{const t=await re.getTransactionHistory({page_size:5,page:s});M.value=t.data.results||[],l.value=t.data.page||1,g.value=Math.ceil((t.data.count||0)/5),B.value=t.data.count||0}catch(t){console.error("Error loading transaction history:",t),M.value=[]}finally{m.value=!1}}function Qe(s){s>=1&&s<=g.value&&Pe(s)}async function ft(){try{const s=await je.getWalletAddress();V.value=s.data.wallet_address||""}catch(s){console.error("Error loading wallet address:",s),V.value=""}}async function vt(){if(!C.value){se(new Error("You are currently offline. Please check your connection and try again."),{action:"saving wallet address"});return}const s=V.value.trim();if(!s){ae.value="Wallet address is required";return}if(!s.startsWith("0x")||s.length!==42){ae.value="Invalid wallet address format. Must be a valid Ethereum address (0x...)";return}J.value=!0,ae.value="",ue();try{await je.setWalletAddress(s),X("Wallet address saved successfully!","success")}catch(t){console.error("Error saving wallet address:",t),se(t,{action:"saving wallet address"})}finally{J.value=!1}}function ht(){Xe(),Pe(),x.value&&v.fetchBillingInfo()}async function Le(){if(h.nsfw_access_mode===2&&d.value.nsfw_access_mode!==2)Z.value=2,ie.value=!0,h.nsfw_access_mode=d.value.nsfw_access_mode??1;else try{await f.updateProfile({nsfw_access_mode:h.nsfw_access_mode}),X("18+ access setting updated successfully","success")}catch(s){console.error("Failed to update NSFW access mode:",s),X("Failed to update 18+ access setting","error"),h.nsfw_access_mode=d.value.nsfw_access_mode??1}}function $e(){ie.value=!1,Z.value=null,h.nsfw_access_mode=d.value.nsfw_access_mode??1}async function yt(){try{await f.updateProfile({nsfw_access_mode:Z.value}),h.nsfw_access_mode=Z.value,X("18+ access setting updated successfully","success")}catch(s){console.error("Failed to update NSFW access mode:",s),X("Failed to update 18+ access setting","error"),h.nsfw_access_mode=d.value.nsfw_access_mode??1}$e()}async function Be(){try{const s=await re.updateProfile({profile_state:ee.value.profile_state});f.user={...f.user,...s.data},y.success("Page visibility updated")}catch(s){console.error("Failed to update page state:",s),y.error("Failed to update page visibility"),ee.value.profile_state=f.user?.profile_state??1}}async function xe(){he.value=!0;try{const s=await Ie.getMyViewers();_.value=s.data}catch(s){console.error("Failed to fetch viewers:",s),y.error("Failed to load viewers")}finally{he.value=!1}}async function bt(){we.value=!0;try{const s={access_level:Y.value.accessLevel};if(Y.value.expiresAt){const t=new Date,k=new Date(Y.value.expiresAt);k.setHours(t.getHours(),t.getMinutes(),t.getSeconds()),s.expires_at=k.toISOString()}await Ie.grantAccessByUsername(Y.value.viewerUsername,s),y.success("Access granted successfully!"),Y.value={viewerUsername:"",accessLevel:"FREE",expiresAt:Ke()},await xe()}catch(s){console.error("Failed to grant access:",s),y.error("Failed to grant access. Please check the username and try again.")}finally{we.value=!1}}async function wt(s){const t=prompt("Enter new access level (FREE, MEMBER, VIP):");if(!t)return;const k=prompt("Enter expiration date (YYYY-MM-DD) or leave empty:");try{const a={access_level:t.toUpperCase()};k&&(a.expires_at=new Date(k).toISOString()),await Ie.grantAccessByUsername(s.username,a),y.success("Access updated successfully!"),await xe()}catch(a){console.error("Failed to update access:",a),y.error("Failed to update access")}}async function _t(s){if(confirm("Are you sure you want to revoke access for this viewer?"))try{await Ie.revokeAccessByUsername(s.username),y.success("Access revoked successfully!"),await xe()}catch(t){console.error("Failed to revoke access:",t),y.error("Failed to revoke access")}}async function kt(){_e.value=!0;try{const s=await re.updateProfile({subscriptions_enabled:H.value.subscriptions_enabled,member_price:H.value.subscriptions_enabled?H.value.member_price:null,vip_price:H.value.subscriptions_enabled?H.value.vip_price:null});f.user={...f.user,...s.data},y.success("Subscription settings updated successfully!")}catch(s){console.error("Failed to update subscription settings:",s),y.error("Failed to update subscription settings. Please try again.")}finally{_e.value=!1}}async function St(){Ne.value=!0,De.value=!0;try{const s=await re.getFollowers();Te.value=s.data.results||[]}catch(s){console.error("Failed to fetch followers:",s),y.error("Failed to load followers")}finally{De.value=!1}}function Je(){Ne.value=!1}
1742async function xt(){try{const s=await re.getEmailSharingPreference();le.value=s.data.share_email_with_creators!==!1}catch(s){console.error("Failed to load email sharing preference:",s)}}async function Ct(){Re.value=!0;try{const s=await re.updateEmailSharingPreference(le.value);le.value=s.data.share_email_with_creators,y.success(s.data.message||"Email sharing preference updated")}catch(s){console.error("Failed to update email sharing preference:",s),le.value=!le.value,y.error("Failed to update email sharing preference")}finally{Re.value=!1}}let Ge=null;return Ye(async()=>{Ge=N.addListener(t=>{C.value=t.isOnline}),d.value&&(h.bio=d.value.bio||"",h.email=d.value.email||"",h.watermark_text=d.value.watermark_text||"",h.avatar=d.value.avatar||null,h.nsfw_access_mode=d.value.nsfw_access_mode??1,h.notification_preference=d.value.notification_preference||"ALL",xt()),Xe(),Pe(),x.value&&(ft(),v.fetchBillingInfo(),xe(),f.user&&(H.value={subscriptions_enabled:f.user.subscriptions_enabled||!1,member_price:f.user.member_price||null,vip_price:f.user.vip_price||null},ee.value={profile_state:f.user.profile_state??1})),await qe(),ze();const s=(c.hash||window.location.hash||"").replace(/^#/,"");s&&T.value.some(t=>t.id===s)&&setTimeout(()=>Me(s),50)}),st(()=>{Ge&&Ge(),I.value&&I.value.disconnect()}),Ee(x,async()=>{await qe(),ze()}),Ee(()=>c.hash,s=>{const t=(s||"").replace(/^#/,"");t&&T.value.some(k=>k.id===t)&&qe(()=>Me(t))}),(s,t)=>{const k=Pt("router-link");return n(),r("div",qs,[me(Gt),e("div",Vs,[t[108]||(t[108]=e("h1",{class:"page-title"},"Settings",-1)),e("nav",Ws,[(n(!0),r(K,null,ge(T.value,a=>(n(),r("a",{key:a.id,href:`#${a.id}`,class:te({active:D.value===a.id}),onClick:ce(ne=>Me(a.id),["prevent"])},i(a.label),11,Hs))),128))]),e("section",js,[t[60]||(t[60]=e("h2",{class:"section-heading"},"Profile",-1)),F.value?b("",!0):(n(),r("div",Ys,[t[39]||(t[39]=e("h3",{class:"card-title"},"Profile Picture",-1)),e("div",Ks,[e("div",zs,[h.avatar?(n(),r("img",{key:0,src:h.avatar,alt:"Profile picture",class:"avatar-image",loading:"lazy"},null,8,Xs)):(n(),r("div",Qs,i(d.value?.username?.[0]?.toUpperCase()||"?"),1))]),e("div",Js,[e("input",{ref_key:"avatarInput",ref:U,type:"file",accept:"image/*",onChange:dt,class:"hidden"},null,544),e("button",{onClick:t[0]||(t[0]=a=>s.$refs.avatarInput.click()),class:"btn btn-primary btn-touch"}," Choose Image "),t[38]||(t[38]=e("p",{class:"recommended-text"}," Recommended: Square image, at least 200x200 pixels ",-1))])])])),e("div",Zs,[t[40]||(t[40]=e("h3",{class:"card-title"},"Biography",-1)),R(e("textarea",{"onUpdate:modelValue":t[1]||(t[1]=a=>h.bio=a),rows:"5",placeholder:"Tell us about yourself...",class:"input-touch",onBlur:t[2]||(t[2]=a=>de("bio",h.bio))},null,544),[[j,h.bio]])]),e("div",en,[t[41]||(t[41]=e("h3",{class:"card-title"},"Email",-1)),R(e("input",{"onUpdate:modelValue":t[3]||(t[3]=a=>h.email=a),type:"email",placeholder:"[email protected]",class:"input-touch",onBlur:t[4]||(t[4]=a=>de("email",h.email))},null,544),[[j,h.email]])]),e("div",tn,[t[43]||(t[43]=e("h3",{class:"card-title"},"Share Email with Creators",-1)),t[44]||(t[44]=e("p",{class:"help-text"}," When you follow a creator, they can see your email so they can contact you. Turn this off to keep your email private from creators you follow. ",-1)),e("label",an,[R(e("input",{type:"checkbox","onUpdate:modelValue":t[5]||(t[5]=a=>le.value=a),class:"checkbox-input",disabled:Re.value,onChange:Ct},null,40,sn),[[et,le.value]]),t[42]||(t[42]=L(" Share my email with creators I follow ",-1))])]),e("div",nn,[t[49]||(t[49]=e("h3",{class:"card-title"},"Notification Preferences",-1)),t[50]||(t[50]=e("p",{class:"help-text"}," Choose which notifications you want to receive. Browser push notifications will be sent based on your preference. ",-1)),e("div",rn,[e("label",on,[R(e("input",{type:"radio","onUpdate:modelValue":t[6]||(t[6]=a=>h.notification_preference=a),value:"SILENT",class:"radio-input",onChange:t[7]||(t[7]=a=>de("notification_preference",h.notification_preference))},null,544),[[Q,h.notification_preference]]),t[45]||(t[45]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Silent (None)"),e("div",{class:"radio-description"},"No notifications will be sent")],-1))]),e("label",ln,[R(e("input",{type:"radio","onUpdate:modelValue":t[8]||(t[8]=a=>h.notification_preference=a),value:"CRITICAL",class:"radio-input",onChange:t[9]||(t[9]=a=>de("notification_preference",h.notification_preference))},null,544),[[Q,h.notification_preference]]),t[46]||(t[46]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Critical (Account Issues)"),e("div",{class:"radio-description"},"Only critical account-related notifications")],-1))]),e("label",dn,[R(e("input",{type:"radio","onUpdate:modelValue":t[10]||(t[10]=a=>h.notification_preference=a),value:"MESSAGES",class:"radio-input",onChange:t[11
1742]||(t[11]=a=>de("notification_preference",h.notification_preference))},null,544),[[Q,h.notification_preference]]),t[47]||(t[47]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Messages (Only Messages and Critical)"),e("div",{class:"radio-description"},"Messages and critical notifications only")],-1))]),e("label",cn,[R(e("input",{type:"radio","onUpdate:modelValue":t[12]||(t[12]=a=>h.notification_preference=a),value:"ALL",class:"radio-input",onChange:t[13]||(t[13]=a=>de("notification_preference",h.notification_preference))},null,544),[[Q,h.notification_preference]]),t[48]||(t[48]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"All (All Notifications)"),e("div",{class:"radio-description"},"Receive all notifications (default)")],-1))])])]),e("div",un,[t[54]||(t[54]=e("h3",{class:"card-title"},"18+ Content Access",-1)),t[55]||(t[55]=e("p",{class:"help-text"}," Control your access to adult content on the platform. ",-1)),e("div",pn,[e("label",mn,[R(e("input",{type:"radio",value:0,"onUpdate:modelValue":t[14]||(t[14]=a=>h.nsfw_access_mode=a),class:"radio-input",onChange:Le},null,544),[[Q,h.nsfw_access_mode]]),t[51]||(t[51]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Hidden"),e("div",{class:"radio-description"},"Do not show NSFW creators in Discover")],-1))]),e("label",gn,[R(e("input",{type:"radio",value:1,"onUpdate:modelValue":t[15]||(t[15]=a=>h.nsfw_access_mode=a),class:"radio-input",onChange:Le},null,544),[[Q,h.nsfw_access_mode]]),t[52]||(t[52]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"SFW Mode"),e("div",{class:"radio-description"},"Show NSFW creators as restricted tiles in Discover")],-1))]),e("label",fn,[R(e("input",{type:"radio",value:2,"onUpdate:modelValue":t[16]||(t[16]=a=>h.nsfw_access_mode=a),class:"radio-input",onChange:Le},null,544),[[Q,h.nsfw_access_mode]]),t[53]||(t[53]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"NSFW Mode"),e("div",{class:"radio-description"},"Full access to all adult content")],-1))])])]),e("div",vn,[t[59]||(t[59]=e("h3",{class:"card-title"},"Change Password",-1)),e("div",hn,[e("div",null,[t[56]||(t[56]=e("label",null,"Current Password",-1)),R(e("input",{"onUpdate:modelValue":t[17]||(t[17]=a=>W.old_password=a),type:"password",class:"input-touch"},null,512),[[j,W.old_password]])]),e("div",null,[t[57]||(t[57]=e("label",null,"New Password",-1)),R(e("input",{"onUpdate:modelValue":t[18]||(t[18]=a=>W.new_password=a),type:"password",class:"input-touch"},null,512),[[j,W.new_password]])]),e("div",null,[t[58]||(t[58]=e("label",null,"Confirm New Password",-1)),R(e("input",{"onUpdate:modelValue":t[19]||(t[19]=a=>W.new_password2=a),type:"password",class:"input-touch"},null,512),[[j,W.new_password2]])]),e("div",yn,[e("button",{onClick:pt,disabled:$.value,class:"btn btn-primary btn-touch"},i($.value?"Changing...":"Change Password"),9,bn),e("button",{onClick:mt,class:"btn btn-secondary btn-touch"}," Logout ")])])])]),x.value?(n(),r("section",wn,[t[68]||(t[68]=e("h2",{class:"section-heading"},"Creator Settings",-1)),e("div",_n,[t[61]||(t[61]=e("h3",{class:"card-title"},"Watermark",-1)),t[62]||(t[62]=e("p",{class:"help-text"}," Add a watermark text that will appear on your media. Leave empty to disable watermarks. ",-1)),R(e("input",{"onUpdate:modelValue":t[20]||(t[20]=a=>h.watermark_text=a),type:"text",placeholder:"e.g., @yourusername",maxlength:"100",class:"input-touch",onBlur:t[21]||(t[21]=a=>de("watermark_text",h.watermark_text||""))},null,544),[[j,h.watermark_text]])]),e("div",kn,[t[66]||(t[66]=e("h3",{class:"card-title"},"Page Visibility",-1)),t[67]||(t[67]=e("p",{class:"help-text"}," Control who can find and visit your creator page. ",-1)),e("div",Sn,[e("label",xn,[R(e("input",{type:"radio",value:0,"onUpdate:modelValue":t[22]||(t[22]=a=>ee.value.profile_state=a),class:"radio-input",onChange:Be},null,544),[[Q,ee.value.profile_state]]),t[63]||(t[63]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Private"),e("div",{class:"radio-description"},"No one can access your profile")],-1))]),e("label",Cn,[R(e("input",{type:"radio",value:1,"onUpdate:modelValue":t[23]||(t[23]=a=>ee.value.profile_state=a),class:"radio-input",onChange:Be},null,544),[[Q,ee.value.profile_state]]),t[64]||(t[64]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"SFW"),e("div",{class:"radio-description"},"Suitable for all ages")],-1))]),e("label",In,[R(e("input",{type:"radio",value:2,"onUpdate:modelValue":t[24]||(t[24]=a=>ee.value.profile_state=a),class:"radio-input",onChange:Be},null,544),[[Q,ee.value.profile_state]]),t[65]||(t[65]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"NSFW"),e("div",{class:"radio-description"},"Suitable only for 18+ audiences")],-1))])])]),e("div",On,[me(Ua)])])):b("",!0),x.value?(n(),r("section",En,[t[79]||(t[79]=e("h2",{class:"section-heading"},"Viewer Details",-1)),e("div",An,[t[73]||(t[73]=e("h3",{class:"card-title"}
1742,"Grant Access to a Viewer",-1)),e("form",{onSubmit:ce(bt,["prevent"]),class:"form-stack"},[e("div",null,[t[69]||(t[69]=e("label",null,"Viewer Username *",-1)),R(e("input",{"onUpdate:modelValue":t[25]||(t[25]=a=>Y.value.viewerUsername=a),type:"text",required:"",placeholder:"Enter viewer username"},null,512),[[j,Y.value.viewerUsername]])]),e("div",null,[t[71]||(t[71]=e("label",null,"Access Level *",-1)),R(e("select",{"onUpdate:modelValue":t[26]||(t[26]=a=>Y.value.accessLevel=a),required:""},[...t[70]||(t[70]=[e("option",{value:"FREE"},"Free",-1),e("option",{value:"MEMBER"},"Member",-1),e("option",{value:"VIP"},"VIP",-1)])],512),[[Ft,Y.value.accessLevel]])]),e("div",null,[t[72]||(t[72]=e("label",null,"Expiration Date (optional)",-1)),R(e("input",{"onUpdate:modelValue":t[27]||(t[27]=a=>Y.value.expiresAt=a),type:"date",min:it()},null,8,Nn),[[j,Y.value.expiresAt]])]),e("button",{type:"submit",disabled:we.value,class:"btn btn-primary"},i(we.value?"Granting...":"Grant Access"),9,Tn)],32)]),e("div",Dn,[t[78]||(t[78]=e("h3",{class:"card-title"},"Current Viewers",-1)),he.value?(n(),r("div",Rn,[...t[74]||(t[74]=[e("div",{class:"loading-spinner"},null,-1)])])):_.value.length===0?(n(),r("div",Un," No viewers yet ")):(n(),r("div",Mn,[(n(!0),r(K,null,ge(_.value,a=>(n(),r("div",{key:a.id,class:"viewer-item"},[e("div",Fn,[e("h4",null,i(a.viewer.username),1),e("p",null,i(a.viewer.email),1)]),e("div",Pn,[e("div",Ln,[t[75]||(t[75]=e("span",{class:"label"},"Access Level:",-1)),e("span",{class:te(["badge",`level-${a.access_level.toLowerCase()}`])},i(a.access_level),3)]),e("div",$n,[t[76]||(t[76]=e("span",{class:"label"},"Granted:",-1)),e("span",null,i(Ue(a.granted_at)),1)]),a.expires_at?(n(),r("div",Bn,[t[77]||(t[77]=e("span",{class:"label"},"Expires:",-1)),e("span",{class:te({expired:a.is_expired})},[L(i(Ue(a.expires_at))+" ",1),a.is_expired?(n(),r("span",Gn,"(Expired)")):b("",!0)],2)])):b("",!0)]),e("div",qn,[e("button",{onClick:ne=>wt(a.viewer),class:"btn-update"},"Update",8,Vn),e("button",{onClick:ne=>_t(a.viewer),class:"btn-revoke"},"Revoke",8,Wn)])]))),128))]))])])):b("",!0),x.value?(n(),r("section",Hn,[t[90]||(t[90]=e("h2",{class:"section-heading"},"Subscription",-1)),e("div",jn,[e("form",{onSubmit:ce(kt,["prevent"]),class:"form-stack"},[e("label",Yn,[R(e("input",{type:"checkbox","onUpdate:modelValue":t[28]||(t[28]=a=>H.value.subscriptions_enabled=a),class:"checkbox-input"},null,512),[[et,H.value.subscriptions_enabled]]),t[80]||(t[80]=L(" Enable Subscriptions ",-1))]),t[89]||(t[89]=e("p",{class:"help-text indented"}," Allow viewers to subscribe to your content for recurring access ",-1)),H.value.subscriptions_enabled?(n(),r("div",Kn,[e("div",zn,[t[83]||(t[83]=e("h4",null,"Member",-1)),e("div",Xn,[t[81]||(t[81]=e("span",{class:"currency"},"$",-1)),R(e("input",{"onUpdate:modelValue":t[29]||(t[29]=a=>H.value.member_price=a),type:"number",step:"0.01",min:"0",placeholder:"0.00",class:"price-input"},null,512),[[j,H.value.member_price,void 0,{number:!0}]]),t[82]||(t[82]=e("span",{class:"period"},"/month",-1))]),t[84]||(t[84]=e("p",{class:"tier-description"},"Basic access to member-only content",-1))]),e("div",Qn,[t[87]||(t[87]=e("h4",null,"VIP",-1)),e("div",Jn,[t[85]||(t[85]=e("span",{class:"currency"},"$",-1)),R(e("input",{"onUpdate:modelValue":t[30]||(t[30]=a=>H.value.vip_price=a),type:"number",step:"0.01",min:"0",placeholder:"0.00",class:"price-input"},null,512),[[j,H.value.vip_price,void 0,{number:!0}]]),t[86]||(t[86]=e("span",{class:"period"},"/month",-1))]),t[88]||(t[88]=e("p",{class:"tier-description"},"Full access including VIP-exclusive content",-1))])])):b("",!0),e("div",Zn,[e("button",{type:"submit",disabled:_e.value,class:"btn btn-primary"},i(_e.value?"Updating...":"Update Settings"),9,er)])],32)])])):b("",!0),e("section",tr,[t[106]||(t[106]=e("h2",{class:"section-heading"},"Billing and Usage",-1)),e("div",ar,[t[92]||(t[92]=e("h3",{class:"card-title"},"Credit Balance",-1)),e("div",sr,[e("div",nr," $"+i(A.value.toFixed(2)),1),t[91]||(t[91]=e("p",{class:"credit-description"},"Your current credit balance",-1)),e("button",{onClick:t[31]||(t[31]=a=>z.value=!0),class:"btn btn-primary add-funds-button btn-touch"}," Add Funds ")])]),x.value?(n(),r("div",rr,[t[94]||(t[94]=e("h3",{class:"card-title"},"Wallet Address",-1)),t[95]||(t[95]=e("p",{class:"help-text"}," Set your USDC wallet address for future withdrawals. This should be a valid Ethereum address on the Base network. ",-1)),e("div",or,[t[93]||(t[93]=e("label",{for:"wallet-address"},"Wallet Address:",-1)),R(e("input",{id:"wallet-address","onUpdate:modelValue":t[32]||(t[32]=a=>V.value=a),type:"text",placeholder:"0x...",class:"wallet-input input-touch",onInput:t[33]||(t[33]=a=>ae.value="")},null,544),[[j,V.value]]),ae.value?(n(),r("p",ir,i(ae.value),1)):b("",!0),e("button",{onClick:vt,disabled:J.value||!V.value.trim(),class:"btn btn-primary save-wallet-button btn-touch"},i(J.value?"Saving...":"Save Wallet Address"),9,lr)])])):b("",!0),x.value?(n(),r("div",dr,[e("div",cr,[t[97]||(t[97]=e("h3",{class:"card-title"},"Usage Overview",-1)),me(k,{to:{name:"CreatorStats",params:{username:d.value?.username}},class:"stats-link"},{default:Ve(()=>[...t[96]||(t[96]=[L("View full statistics â",-1)])]),_:1},8,["to"])]),E(v).loading?(n(),r("div",ur,"Loading billing info...")):E(v).error?(n(),r("div",pr," Error loading billing info: "+i(E(v).error),1)):(n(),r("div",mr,[e("div",gr,[e("div",fr,[t[98]||(t[98]=e("h4",null,"Current Credit",-1)),e("div",vr,"$"+i(ke(E(v).billingInfo.credit_balance)),1),E(v).billingInfo.credit_estimate_days?(n(),r("div",hr," Estimated "+i(E(v).billingInfo.credit_estimate_days)+" days remaining ",1)):E(v).billingInfo.credit_balance<=0?(n(),r("div",yr," Credit depleted - content is private ")):b("",!0)]),e("div",br,[t[99]||(t[99]=e("h4",null,"Storage Usage",-1)),e("div",wr,i(Se(E(v).billingInfo.storage_usage_gb)),1),e("div",_r,"$"+i(ke(E(v).billingInfo.storage_cost))+"/month",1)]),e("div",kr,[t[100]||(t[100]=e("h4",null,"Bandwidth (This Month)",-1)),e("div",Sr,i(Se(E(v).billingInfo.monthly_bandwidth_gb)),1),e("div",xr,"$"+i(ke(E(v).billingInfo.monthly_bandwidth_cost)),1)]),e("div",Cr,[t[101]||(t[101]=e("h4",null,"Followers",-1)),e("div",{class:"usage-amount clickable",onClick:St},i(E(v).billingInfo.follower_count||0),1),t[102]||(t[102]=e("div",{class:"cost-amount"},"Click to view details",-1))])]),e("div",Ir,[t[104]||(t[104]=e("h4",null,"Top Viewers This Month",-1)),E(v).billingInfo.viewer_breakdown?.length?(n(),r("div",Er,[t[103]||(t[103]=e("div",{class:"viewer-header"}
1742,[e("span",null,"Viewer"),e("span",null,"Bandwidth"),e("span",null,"Cost")],-1)),(n(!0),r(K,null,ge(E(v).billingInfo.viewer_breakdown,a=>(n(),r("div",{key:a.viewer__username,class:"viewer-row"},[a.viewer__username==="Public Embeds"?(n(),Ze(k,{key:0,to:{name:"CreatorStats",params:{username:d.value?.username},query:{public:"1"}},class:"public-embeds-link",title:"See which content anonymous visitors viewed and where they came from"},{default:Ve(()=>[L(i(a.viewer__username)+" â",1)]),_:2},1032,["to"])):(n(),r("span",Ar,i(a.viewer__username),1)),e("span",null,i(Se(a.total_bytes/1024**3)),1),e("span",null,"$"+i(ke(a.total_cost)),1)]))),128))])):(n(),r("div",Or," No viewer data available "))])]))])):b("",!0),e("div",Nr,[t[105]||(t[105]=e("h3",{class:"card-title"},"Transaction History",-1)),m.value?(n(),r("div",Tr,"Loading transactions...")):M.value.length===0?(n(),r("div",Dr,"No transactions yet")):(n(),r("div",Rr,[e("div",Ur,[(n(!0),r(K,null,ge(M.value,a=>(n(),r("div",{key:a.id,class:"transaction-item"},[e("div",Mr,[e("div",Fr,i(a.transaction_type_display),1),e("div",{class:te(["transaction-amount",Fe(a)>=0?"amount-positive":"amount-negative"])},i(Fe(a)>=0?"+":"")+"$"+i(Math.abs(Fe(a)).toFixed(2)),3)]),e("div",Pr,[e("div",Lr,[gt(a)?(n(),r("span",$r," To: "+i(a.destination_user.username),1)):(n(),r("span",Br,[a.source_user?(n(),r("span",Gr," From: "+i(a.source_user.username),1)):(n(),r("span",qr,"From: System"))]))]),a.description?(n(),r("div",Vr,i(a.description),1)):b("",!0),e("div",Wr,i(Ue(a.created_at)),1)])]))),128))]),g.value>1?(n(),r("div",Hr,[e("button",{onClick:t[34]||(t[34]=a=>Qe(l.value-1)),disabled:l.value===1||m.value,class:"pagination-btn"}," Previous ",8,jr),e("span",Yr,[L(" Page "+i(l.value)+" of "+i(g.value)+" ",1),B.value>0?(n(),r("span",Kr," ("+i(B.value)+" total)",1)):b("",!0)]),e("button",{onClick:t[35]||(t[35]=a=>Qe(l.value+1)),disabled:l.value===g.value||m.value,class:"pagination-btn"}," Next ",8,zr)])):b("",!0)]))])]),G.value?(n(),r("div",{key:3,class:te(["message",w.value==="success"?"message-success":"message-error"])},i(G.value),3)):b("",!0),O.value?(n(),Ze(Ps,Lt({key:4},O.value,{onRetry:lt,onDismiss:ue,class:"mb-4"}),null,16)):b("",!0),e("p",Xr,[me(k,{to:"/about"},{default:Ve(()=>[...t[107]||(t[107]=[L("About, TOS, and FAQ",-1)])]),_:1})])]),me(ks,{show:z.value,onClose:t[36]||(t[36]=a=>z.value=!1),onCreditUpdated:ht},null,8,["show"]),ie.value?(n(),r("div",{key:0,class:"modal-overlay",onClick:ce($e,["self"])},[e("div",{class:"modal-content nsfw-modal"},[t[109]||(t[109]=e("h3",{class:"modal-title"},"18+ Content Warning",-1)),t[110]||(t[110]=e("div",{class:"modal-body"},[e("p",null,"This site allows access to NSFW content, featuring age-restricted materials such as nudity and explicit sexual depictions."),e("p",null,"By proceeding, you confirm that you are at least 18 years old or the legal adult age in your region, and you agree to view sexually explicit material.")],-1)),e("div",{class:"modal-actions"},[e("button",{class:"btn btn-secondary btn-touch",onClick:$e},"Cancel"),e("button",{class:"btn btn-primary btn-touch",onClick:yt},"Yes, I am 18+")])])])):b("",!0),Ne.value?(n(),r("div",{key:1,class:"modal-overlay",onClick:Je},[e("div",{class:"modal-content followers-modal",onClick:t[37]||(t[37]=ce(()=>{},["stop"]))},[e("div",{class:"modal-header"},[t[111]||(t[111]=e("h3",null,"Followers",-1)),e("button",{class:"modal-close",onClick:Je},"Ã")]),e("div",Qr,[t[113]||(t[113]=e("p",{class:"help-text followers-contact-help"}," Contact emails for followers who share theirs with you. Use these to reach fans directly. ",-1)),De.value?(n(),r("div",Jr,"Loading followers...")):Te.value.length===0?(n(),r("div",Zr,"No followers yet")):(n(),r("div",eo,[t[112]||(t[112]=e("div",{class:"followers-header"},[e("span",null,"Username"),e("span",null,"Email"),e("span",null,"Days Following"),e("span",null,"Bandwidth Used")],-1)),(n(!0),r(K,null,ge(Te.value,a=>(n(),r("div",{key:a.id,class:"follower-row"},[e("div",to,[a.avatar?(n(),r("img",{key:0,src:a.avatar,alt:a.username,class:"follower-avatar"},null,8,ao)):(n(),r("div",so,i(a.username.charAt(0).toUpperCase()),1)),e("span",no,i(a.username),1)]),e("span",ro,[a.email_shared&&a.email?(n(),r("a",{key:0,href:`mailto:${a.email}`,class:"follower-email-link"},i(a.email),9,oo)):(n(),r("span",io,"Hidden"))]),e("span",null,i(a.days_following)+" days",1),e("span",null,i(Se(a.bandwidth_used_gb)),1)]))),128))]))])])])):b("",!0)])}}},yo=Ae(lo,[["__scopeId","data-v-cc25f2f7"]]);export{yo as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.