Commit f8521de0 authored by nextime's avatar nextime

Try to make extensions works fully...

parent abbf8c5f
# Chrome Extension Runtime API Emulation
This directory contains a test extension and implementation for emulating the Chrome Extension Runtime API in our Qt6-based browser.
## Overview
The implementation consists of:
1. **RuntimeBridge** (`assets/browser/js/runtime_bridge.py`): A Python QObject that bridges between the browser and JavaScript
2. **chrome-runtime-api.js** (`assets/browser/js/chrome-runtime-api.js`): JavaScript implementation of the chrome.runtime API
3. **Test Extension** (`assets/browser/extensions/test-extension/`): A simple extension to test the API implementation
## Supported APIs
The following chrome.runtime APIs are supported:
- `chrome.runtime.id` - Returns the extension ID
- `chrome.runtime.getURL()` - Converts a relative path to a fully-qualified extension URL
- `chrome.runtime.getManifest()` - Returns the extension's manifest.json contents
- `chrome.runtime.sendMessage()` - Sends a message to the extension or another extension
- `chrome.runtime.onMessage` - Event fired when a message is received
- `chrome.runtime.connect()` - Creates a connection to the extension or another extension
- `chrome.runtime.onConnect` - Event fired when a connection is made
- `chrome.runtime.getBackgroundPage()` - Returns the background page
- Various lifecycle events: `onInstalled`, `onStartup`, `onSuspend`, `onUpdateAvailable`
## Test Extension
The test extension demonstrates the use of the chrome.runtime API. It includes:
- **Popup** (`popup.html`, `popup.js`): A simple UI to test various chrome.runtime methods
- **Background Script** (`background.js`): A background script that listens for messages and connections
- **Content Script** (`content.js`): A content script that injects a control panel into web pages
## Testing
To test the chrome.runtime API implementation:
1. Run the test script: `python test_extension.py`
2. Click the "Test Extension" button in the toolbar
3. Use the buttons in the popup to test various chrome.runtime API methods
4. Check the browser console for log messages
You can also test the content script by navigating to any website and looking for the control panel in the bottom-right corner.
## Implementation Details
### Extension Loading
Extensions are loaded from the `assets/browser/extensions` directory and copied to the browser profile's extensions directory. The browser scans for extensions on startup and registers them with the runtime bridge.
### URL Scheme Handling
The browser uses a custom URL scheme handler (`qextension://`) to load extension resources. This allows extensions to use relative paths in their manifest and code.
### Message Passing
Message passing is implemented using QWebChannel to communicate between JavaScript and Python. The RuntimeBridge class handles routing messages between different parts of an extension.
### Background Pages
Background pages are loaded in hidden tabs. For service workers, a special HTML page is created that loads the service worker script.
## Limitations
- This is a simplified implementation and does not support all chrome.runtime APIs
- Service workers are not fully supported; they are loaded in a regular page context
- Cross-extension messaging is limited
- Some APIs may behave differently than in Chrome
\ No newline at end of file
5_6_1749796340598
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
(function(a,b){var r=a0b,c=a();while(!![]){try{var d=-parseInt(r(0x177))/0x1*(-parseInt(r(0x18e))/0x2)+parseInt(r(0x19d))/0x3+-parseInt(r(0x183))/0x4*(-parseInt(r(0x182))/0x5)+-parseInt(r(0x189))/0x6+-parseInt(r(0x17d))/0x7*(parseInt(r(0x18d))/0x8)+-parseInt(r(0x178))/0x9+-parseInt(r(0x19c))/0xa*(parseInt(r(0x18f))/0xb);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a0a,0x19149),!(function(){var A=a0b,a,b;a=function(){var s=a0b,c=document[s(0x184)](s(0x19f));c['innerHTML']=s(0x17c),document[s(0x19b)][s(0x194)](c),publicApi(s(0x17e))[s(0x179)](f=>{var t=s,g=f,h=document['querySelector'](t(0x1a0)),i=document['querySelector']('#cbMessageInputSet'),j=document[t(0x190)](t(0x198)),k=document[t(0x190)]('#cbSendMessageSetBtn');g&&void 0x0!==g?(localStorage['setItem']('cbMessageApi',!0x0),window['onunload']=function(){var u=t;localStorage[u(0x188)]('cbMessageApi');},j[t(0x193)](t(0x18b),function(m){var v=t;let p=h[v(0x17b)];p=v(0x199)==typeof p?JSON[v(0x19e)](p):p,g[v(0x18c)](p);}),k[t(0x193)](t(0x18b),function(){var w=t;g[w(0x181)](i['value'],w(0x180));})):j[t(0x193)](t(0x18b),function(){var x=t;window[x(0x197)][x(0x181)](h[x(0x17b)],x(0x180));});let l={'displayName':g['model'][t(0x187)](),'username':g['model'][t(0x191)]()};h[t(0x17a)](t(0x196),JSON['stringify'](l)),g[t(0x193)](t(0x19a),({amount:m,username:p,displayName:q})=>{var y=t;window[y(0x195)]({'type':y(0x19a),'from':y(0x17f),'tip':{'amount':m,'tipperName':q,'username':p,'cParameter':''}});}),g[t(0x193)](t(0x185),({message:m})=>{var z=t;window['postMessage']({'type':z(0x192),'from':'bongacams','message':m});});});},'interactive'===(b=document[A(0x186)])||A(0x18a)===b?a():window[A(0x193)](A(0x176),a);}()));function a0b(a,b){var c=a0a();return a0b=function(d,e){d=d-0x176;var f=c[d];return f;},a0b(a,b);}function a0a(){var B=['1483155beMQRr','then','setAttribute','value','\x0a\x20\x20\x20\x20\x20\x20<input\x20type=\x27button\x27\x20style=\x27display:\x20none;\x27\x20id=\x27cbSendMessageBtn\x27/>\x0a\x20\x20\x20\x20\x20\x20<input\x20type=\x27text\x27\x20style=\x27display:none;\x27\x20id=\x27cbMessageInput\x27/>\x0a\x20\x20\x20\x20\x20\x20<input\x20type=\x27button\x27\x20style=\x27display:\x20none;\x27\x20id=\x27cbSendMessageSetBtn\x27/>\x0a\x20\x20\x20\x20\x20\x20<input\x20type=\x27text\x27\x20style=\x27display:none;\x27\x20id=\x27cbMessageInputSet\x27/>\x0a\x20\x20\x20\x20','21xZbwoS','lovense','bongacams','btn','sendMessage','5yCBsZF','820288PDCiyg','createElement','order','readyState','getDisplayName','removeItem','394380wJoPNa','complete','click','send','54744HXmhJK','4arBiLz','35079ZLUbyQ','querySelector','getUsername','orderMessage','addEventListener','appendChild','postMessage','data','chat','#cbSendMessageBtn','string','tip','body','40acdosV','410037eFFxca','parse','div','#cbMessageInput','DOMContentLoaded','12394RshGtw'];a0a=function(){return B;};return a0a();}
\ No newline at end of file
function a1b(a,b){var c=a1a();return a1b=function(d,e){d=d-0x112;var f=c[d];return f;},a1b(a,b);}function a1a(){var i=['760588ipeXxo','5eLbVuT','sendMessage','1086620GaZhHN','14071360qeKQTq','3085188xfUxLF','#cbMessageInput','click','4290930gpYhql','265786GqqelY','chat','1475229EwekQg','val'];a1a=function(){return i;};return a1a();}var a1g=a1b;(function(a,b){var f=a1b,c=a();while(!![]){try{var d=parseInt(f(0x11e))/0x1+parseInt(f(0x11a))/0x2+parseInt(f(0x11c))/0x3+-parseInt(f(0x114))/0x4+-parseInt(f(0x112))/0x5*(-parseInt(f(0x116))/0x6)+parseInt(f(0x119))/0x7+-parseInt(f(0x115))/0x8;if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a1a,0x75a2d),$('#cbSendMessageBtn')['on'](a1g(0x118),function(){var h=a1g;Chat[h(0x113)]($(h(0x117))[h(0x11d)](),h(0x11b));}));
\ No newline at end of file
var a2g=a2b;(function(a,b){var f=a2b,c=a();while(!![]){try{var d=-parseInt(f(0xd2))/0x1+-parseInt(f(0xdc))/0x2*(-parseInt(f(0xd0))/0x3)+parseInt(f(0xda))/0x4+parseInt(f(0xd5))/0x5+-parseInt(f(0xd6))/0x6+-parseInt(f(0xd8))/0x7*(-parseInt(f(0xd3))/0x8)+parseInt(f(0xdd))/0x9*(parseInt(f(0xdb))/0xa);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a2a,0x51a8c),$('#cbSendMessageBtn')['on'](a2g(0xd9),function(){var h=a2g;window[h(0xd7)][h(0xd1)][h(0xcf)]($(h(0xd4))[h(0xce)]());}));function a2b(a,b){var c=a2a();return a2b=function(d,e){d=d-0xce;var f=c[d];return f;},a2b(a,b);}function a2a(){var i=['3818034CgqBbR','TSHandler','21ooRqWG','click','601348owEtCZ','3210jtUWqn','1134062buHlnR','7785TbEeDn','val','send_room_message','3xeiOLi','message_outbound','528920WBDWzj','428776uFDiLB','#cbMessageInput','1719555AjOjFO'];a2a=function(){return i;};return a2a();}
\ No newline at end of file
function a3b(a,b){var c=a3a();return a3b=function(d,e){d=d-0x1f4;var f=c[d];return f;},a3b(a,b);}(function(a,b){var f=a3b,c=a();while(!![]){try{var d=-parseInt(f(0x208))/0x1+-parseInt(f(0x1fd))/0x2+-parseInt(f(0x1fa))/0x3+parseInt(f(0x202))/0x4+parseInt(f(0x1fc))/0x5*(parseInt(f(0x205))/0x6)+parseInt(f(0x204))/0x7*(parseInt(f(0x1f5))/0x8)+-parseInt(f(0x1fe))/0x9;if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a3a,0x21ae7),!(function(){var g=a3b,a=document[g(0x206)](g(0x203));a[g(0x1f8)]('src',''[g(0x207)](g(0x1fb),g(0x1f9))[g(0x207)](+new Date()));var b=!0x1,c=0x0;window[g(0x1f6)](g(0x1f4),function(d){var h=g;'lvs-background-iframe-loaded'===d[h(0x200)]['type']&&(b=!0x0);}),setInterval(function(){var i=g;!b&&c<0x1e?(c++,a['setAttribute'](i(0x201),''['concat']('https://extension.lovense.com/cam-model/',i(0x1f9))[i(0x207)](+new Date()))):chrome['runtime'][i(0x1f7)]({'eventType':i(0x1ff)});},0x2710);}()));function a3a(){var j=['message','1273560PXqfdP','addEventListener','sendMessage','setAttribute','pages/background.html?t=','377580eKTVqb','https://extension.lovense.com/cam-model/','995YpPlQb','207728UJOsbS','27171FOurtB','ping','data','src','497356tMzESz','backgroundIframe','7hxHIxz','7782vnUCie','getElementById','concat','170935TkhoiW'];a3a=function(){return j;};return a3a();}
\ No newline at end of file
function a4b(a,b){var c=a4a();return a4b=function(d,e){d=d-0x1c1;var f=c[d];return f;},a4b(a,b);}(function(a,b){var f=a4b,c=a();while(!![]){try{var d=parseInt(f(0x1c6))/0x1*(parseInt(f(0x1d2))/0x2)+-parseInt(f(0x1cd))/0x3+parseInt(f(0x1ea))/0x4*(parseInt(f(0x1c7))/0x5)+parseInt(f(0x1d0))/0x6*(-parseInt(f(0x1e4))/0x7)+-parseInt(f(0x1c8))/0x8*(parseInt(f(0x1c2))/0x9)+-parseInt(f(0x1e3))/0xa*(-parseInt(f(0x1dd))/0xb)+parseInt(f(0x1e6))/0xc;if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a4a,0x8b83d),!(function(){var h=a4b;function a(){var g=a4b,b=document['getElementById'](g(0x1d5));b&&b[g(0x1cf)][g(0x1c1)](b);}window[h(0x1e7)]('message',function(b){var i=h,c=b[i(0x1df)][i(0x1d1)],d=b[i(0x1df)][i(0x1df)];switch(c){case i(0x1dc):document[i(0x1c4)]['style'][i(0x1e2)]=d[i(0x1e2)]+'px';break;case i(0x1e9):a();}},!0x1),window['onload']=function(){var j=h;if('zh-CN'!==navigator[j(0x1d7)]&&j(0x1d9)!==navigator[j(0x1d7)]&&'zh'!==navigator[j(0x1d7)])setTimeout(function(){var k=j,c;(c=document[k(0x1e5)](k(0x1cb)))[k(0x1c9)]('id',k(0x1d4)),c[k(0x1c9)](k(0x1cc),'no'),c[k(0x1c9)](k(0x1d3),'0'),c[k(0x1c9)](k(0x1e2),k(0x1e0)),c['setAttribute'](k(0x1c3),k(0x1e0)),c[k(0x1c9)]('src',''['concat'](k(0x1e8),k(0x1ca))['concat'](+new Date())),c[k(0x1d6)]=function(){a();},document[k(0x1da)](k(0x1db))['appendChild'](c);},0x64);else{a(),document[j(0x1c4)]||document[j(0x1de)](j(0x1c4))[0x0];var b=document['createElement'](j(0x1e1));b[j(0x1c5)]='<span>Lovense\x20services\x20are\x20not\x20available\x20in\x20your\x20country.</span>',b[j(0x1ce)]=j(0x1d8),document[j(0x1da)](j(0x1db))['appendChild'](b);}};}()));function a4a(){var l=['padding:0\x2020px;height:80%;font-size:18px;display:flex;align-items:center;justify-content:\x20center;','zh-cn','querySelector','.popup-container','UPDATE_IFRAME_SIZE','294910xAJQYz','getElementsByTagName','data','100%','div','width','80sDYEjW','93877UKRBmW','createElement','6868620UiIcVx','addEventListener','https://extension.lovense.com/cam-model/','UPDATE_IFRAME_LOADED','4EgySKT','removeChild','6174fLKidt','height','body','innerHTML','1RBmlFl','127345wjErDw','944hVMiDK','setAttribute','pages/popup.html?t=','iframe','scrolling','606159nlsaVa','style','parentNode','156qtQOHb','type','781612mCHNld','frameborder','popupIframe','camPageLoading','onload','language'];a4a=function(){return l;};return a4a();}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
function a7b(a,b){var c=a7a();return a7b=function(d,e){d=d-0x17c;var f=c[d];return f;},a7b(a,b);}(function(a,b){var aw=a7b,c=a();while(!![]){try{var d=parseInt(aw(0x1ef))/0x1*(-parseInt(aw(0x195))/0x2)+parseInt(aw(0x1f0))/0x3+-parseInt(aw(0x1f6))/0x4+-parseInt(aw(0x1dd))/0x5+-parseInt(aw(0x224))/0x6+parseInt(aw(0x25c))/0x7*(-parseInt(aw(0x19e))/0x8)+-parseInt(aw(0x1c1))/0x9*(-parseInt(aw(0x19b))/0xa);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a7a,0x464d2),!(function(){var a={0xe9a:function(d){var az=a7b;function f(g){var ax=a7b;return d[ax(0x18c)]=f=ax(0x226)==typeof Symbol&&'symbol'==typeof Symbol[ax(0x1a9)]?function(h){return typeof h;}:function(h){var ay=ax;return h&&ay(0x226)==typeof Symbol&&h[ay(0x1e1)]===Symbol&&h!==Symbol[ay(0x22a)]?ay(0x1c7):typeof h;},d['exports']['__esModule']=!0x0,d[ax(0x18c)]['default']=d[ax(0x18c)],f(g);}d[az(0x18c)]=f,d['exports'][az(0x212)]=!0x0,d['exports'][az(0x218)]=d[az(0x18c)];},0x1219:function(d,f,g){var aA=a7b,h=g(0xe9a)[aA(0x218)];function i(){'use strict';var aB=aA;d['exports']=i=function(){return z;},d['exports'][aB(0x212)]=!0x0,d['exports'][aB(0x218)]=d['exports'];var q,z={},A=Object[aB(0x22a)],B=A[aB(0x1de)],C=Object[aB(0x203)]||function(ae,af,ag){ae[af]=ag['value'];},D=aB(0x226)==typeof Symbol?Symbol:{},F=D[aB(0x1a9)]||aB(0x1fa),G=D[aB(0x193)]||'@@asyncIterator',H=D[aB(0x1db)]||'@@toStringTag';function I(ae,af,ag){var aC=aB;return Object[aC(0x203)](ae,af,{'value':ag,'enumerable':!0x0,'configurable':!0x0,'writable':!0x0}),ae[af];}try{I({},'');}catch(ae){I=function(af,ag,ah){return af[ag]=ah;};}function J(af,ag,ah,ai){var aD=aB,aj=ag&&ag[aD(0x22a)]instanceof Z?ag:Z,ak=Object[aD(0x21c)](aj[aD(0x22a)]),al=new ac(ai||[]);return C(ak,aD(0x20c),{'value':a8(af,ah,al)}),ak;}function K(af,ag,ah){var aE=aB;try{return{'type':aE(0x1a1),'arg':af[aE(0x181)](ag,ah)};}catch(ai){return{'type':aE(0x1c4),'arg':ai};}}z[aB(0x1b0)]=J;var Q=aB(0x22b),U=aB(0x1a0),V=aB(0x1e4),X=aB(0x1c9),Y={};function Z(){}function a0(){}function a1(){}var a2={};I(a2,F,function(){return this;});var a3=Object['getPrototypeOf'],a4=a3&&a3(a3(ad([])));a4&&a4!==A&&B[aB(0x181)](a4,F)&&(a2=a4);var a5=a1[aB(0x22a)]=Z[aB(0x22a)]=Object[aB(0x21c)](a2);function a6(af){var aF=aB;[aF(0x231),'throw','return']['forEach'](function(ag){I(af,ag,function(ah){var aG=a7b;return this[aG(0x20c)](ag,ah);});});}function a7(af,ag){var aL=aB;function ah(aj,ak,al,am){var aH=a7b,an=K(af[aj],af,ak);if(aH(0x1c4)!==an[aH(0x1f1)]){var ao=an[aH(0x1a7)],ap=ao[aH(0x19c)];return ap&&aH(0x24c)==h(ap)&&B[aH(0x181)](ap,aH(0x23b))?ag[aH(0x244)](ap[aH(0x23b)])[aH(0x1e2)](function(aq){var aI=aH;ah(aI(0x231),aq,al,am);},function(aq){var aJ=aH;ah(aJ(0x1c4),aq,al,am);}):ag[aH(0x244)](ap)['then'](function(aq){var aK=aH;ao[aK(0x19c)]=aq,al(ao);},function(aq){return ah('throw',aq,al,am);});}am(an[aH(0x1a7)]);}var ai;C(this,aL(0x20c),{'value':function(aj,ak){var aM=aL;function al(){return new ag(function(am,an){ah(aj,ak,am,an);});}return ai=ai?ai[aM(0x1e2)](al,al):al();}});}function a8(af,ag,ah){var ai=Q;return function(aj,ak){var aN=a7b;if(ai===V)throw Error(aN(0x248));if(ai===X){if('throw'===aj)throw ak;return{'value':q,'done':!0x0};}for(ah[aN(0x1e9)]=aj,ah[aN(0x1a7)]=ak;;){var al=ah[aN(0x204)];if(al){var am=a9(al,ah);if(am){if(am===Y)continue;return am;}}if(aN(0x231)===ah[aN(0x1e9)])ah[aN(0x1ca)]=ah[aN(0x202)]=ah['arg'];else{if('throw'===ah['method']){if(ai===Q)throw ai=X,ah['arg'];ah[aN(0x1bd)](ah[aN(0x1a7)]);}else aN(0x241)===ah[aN(0x1e9)]&&ah['abrupt']('return',ah[aN(0x1a7)]);}ai=V;var an=K(af,ag,ah);if(aN(0x1a1)===an['type']){if(ai=ah[aN(0x20d)]?X:U,an['arg']===Y)continue;return{'value':an[aN(0x1a7)],'done':ah['done']};}aN(0x1c4)===an['type']&&(ai=X,ah['method']='throw',ah[aN(0x1a7)]=an[aN(0x1a7)]);}};}function a9(af,ag){var aO=aB,ah=ag[aO(0x1e9)],ai=af[aO(0x1a9)][ah];if(ai===q)return ag[aO(0x204)]=null,aO(0x1c4)===ah&&af['iterator'][aO(0x241)]&&(ag[aO(0x1e9)]=aO(0x241),ag[aO(0x1a7)]=q,a9(af,ag),aO(0x1c4)===ag['method'])||aO(0x241)!==ah&&(ag[aO(0x1e9)]='throw',ag[aO(0x1a7)]=new TypeError(aO(0x21e)+ah+aO(0x246))),Y;var aj=K(ai,af[aO(0x1a9)],ag['arg']);if(aO(0x1c4)===aj[aO(0x1f1)])return ag[aO(0x1e9)]=aO(0x1c4),ag[aO(0x1a7)]=aj[aO(0x1a7)],ag['delegate']=null,Y;var ak=aj[aO(0x1a7)];return ak?ak[aO(0x20d)]?(ag[af['resultName']]=ak[aO(0x19c)],ag['next']=af[aO(0x1c0)],aO(0x241)!==ag['method']&&(ag['method']='next',ag['arg']=q),ag['delegate']=null,Y):ak:(ag[aO(0x1e9)]=aO(0x1c4),ag[aO(0x1a7)]=new TypeError('iterator\x20result\x20is\x20not\x20an\x20object'),ag[aO(0x204)]=null,Y);}function aa(af){var aP=aB,ag={'tryLoc':af[0x0]};0x1 in af&&(ag[aP(0x25a)]=af[0x1]),0x2 in af&&(ag[aP(0x24d)]=af[0x2],ag['afterLoc']=af[0x3]),this[aP(0x17e)][aP(0x1b9)](ag);}function ab(af){var aQ=aB,ag=af[aQ(0x249)]||{};ag[aQ(0x1f1)]=aQ(0x1a1),delete ag[aQ(0x1a7)],af[aQ(0x249)]=ag;}function ac(af){var aR=aB;this[aR(0x17e)]=[{'tryLoc':'root'}],af[aR(0x21a)](aa,this),this['reset'](!0x0);}function ad(af){var aS=aB;if(af||''===af){var ag=af[F];if(ag)return ag['call'](af);if(aS(0x226)==typeof af[aS(0x231)])return af;if(!isNaN(af[aS(0x1ba)])){var ah=-0x1,ai=function aj(){var aT=aS;for(;++ah<af['length'];)if(B[aT(0x181)](af,ah))return aj[aT(0x19c)]=af[ah],aj[aT(0x20d)]=!0x1,aj;return aj[aT(0x19c)]=q,aj['done']=!0x0,aj;};return ai[aS(0x231)]=ai;}}throw new TypeError(h(af)+aS(0x222));}return a0[aB(0x22a)]=a1,C(a5,aB(0x1e1),{'value':a1,'configurable':!0x0}),C(a1,aB(0x1e1),{'value':a0,'configurable':!0x0}),a0[aB(0x1da)]=I(a1,H,aB(0x1f9)),z[aB(0x22f)]=function(af){var aU=aB,ag=aU(0x226)==typeof af&&af[aU(0x1e1)];return!!ag&&(ag===a0||aU(0x1f9)===(ag[aU(0x1da)]||ag[aU(0x17f)]));},z['mark']=function(af){var aV=aB;return Object[aV(0x234)]?Object[aV(0x234)](af,a1):(af[aV(0x200)]=a1,I(af,H,aV(0x1f9))),af['prototype']=Object[aV(0x21c)](a5),af;},z[aB(0x1ea)]=function(af){return{'__await':af};},a6(a7[aB(0x22a)]),I(a7['prototype'],G,function(){return this;}),z['AsyncIterator']=a7,z[aB(0x187)]=function(af,ag,ah,ai,aj){var aW=aB;void 0x0===aj&&(aj=Promise);var ak=new a7(J(af,ag,ah,ai),aj);return z[aW(0x22f)](ag)?ak:ak[aW(0x231)]()[aW(0x1e2)](function(al){var aX=aW;return al[aX(0x20d)]?al[aX(0x19c)]:ak[aX(0x231)]();});},a6(a5),I(a5,H,aB(0x1a2)),I(a5,F,function(){return this;}),I(a5,aB(0x216),function(){var aY=aB;return aY(0x25b);}),z[aB(0x1fb)]=function(af){var aZ=aB,ag=Object(af),ah=[];for(var ai in ag)ah[aZ(0x1b9)](ai);return ah['reverse'](),function aj(){var b0=aZ;for(;ah[b0(0x1ba)];){var ak=ah[b0(0x24a)]();if(ak in ag)return aj['value']=ak,aj[b0(0x20d)]=!0x1,aj;}return aj['done']=!0x0,aj;};},z[aB(0x1cb)]=ad,ac[aB(0x22a)]={'constructor':ac,'reset':function(af){var b1=aB;if(this[b1(0x1aa)]=0x0,this[b1(0x231)]=0x0,this[b1(0x1ca)]=this[b1(0x202)]=q,this['done']=!0x1,this['delegate']=null,this[b1(0x1e9)]=b1(0x231),this[b1(0x1a7)]=q,this[b1(0x17e)][b1(0x21a)](ab),!af){for(var ag in this)'t'===ag[b1(0x1be)](0x0)&&B[b1(0x181)](this,ag)&&!isNaN(+ag[b1(0x207)](0x1))&&(this[ag]=q);}},'stop':function(){var b2=aB;this['done']=!0x0;var af=this['tryEntries'][0x0][b2(0x249)];if(b2(0x1c4)===af[b2(0x1f1)])throw af['arg'];return this['rval'];},'dispatchException':function(af){var b4=aB;if(this['done'])throw af;var ag=this;function ah(an,ao){var b3=a7b;return ak[b3(0x1f1)]=b3(0x1c4),ak[b3(0x1a7)]=af,ag[b3(0x231)]=an,ao&&(ag['method']=b3(0x231),ag[b3(0x1a7)]=q),!!ao;}for(var ai=this['tryEntries'][b4(0x1ba)]-0x1;ai>=0x0;--ai){var aj=this[b4(0x17e)][ai],ak=aj[b4(0x249)];if(b4(0x1a5)===aj[b4(0x1f4)])return ah(b4(0x1e6));if(aj[b4(0x1f4)]<=this[b4(0x1aa)]){var al=B[b4(0x181)](aj,'catchLoc'),am=B[b4(0x181)](aj,b4(0x24d));if(al&&am){if(this['prev']<aj[b4(0x25a)])return ah(aj[b4(0x25a)],!0x0);if(this[b4(0x1aa)]<aj[b4(0x24d)])return ah(aj[b4(0x24d)]);}else{if(al){if(this[b4(0x1aa)]<aj[b4(0x25a)])return ah(aj[b4(0x25a)],!0x0);}else{if(!am)throw Error('try\x20statement\x20without\x20catch\x20or\x20finally');if(this[b4(0x1aa)]<aj[b4(0x24d)])return ah(aj['finallyLoc']);}}}}},'abrupt':function(af,ag){var b5=aB;for(var ah=this[b5(0x17e)][b5(0x1ba)]-0x1;ah>=0x0;--ah){var ai=this[b5(0x17e)][ah];if(ai[b5(0x1f4)]<=this[b5(0x1aa)]&&B['call'](ai,b5(0x24d))&&this['prev']<ai['finallyLoc']){var aj=ai;break;}}aj&&('break'===af||b5(0x185)===af)&&aj[b5(0x1f4)]<=ag&&ag<=aj[b5(0x24d)]&&(aj=null);var ak=aj?aj[b5(0x249)]:{};return ak[b5(0x1f1)]=af,ak[b5(0x1a7)]=ag,aj?(this[b5(0x1e9)]='next',this[b5(0x231)]=aj[b5(0x24d)],Y):this[b5(0x25e)](ak);},'complete':function(af,ag){var b6=aB;if(b6(0x1c4)===af[b6(0x1f1)])throw af['arg'];return b6(0x1f2)===af[b6(0x1f1)]||'continue'===af[b6(0x1f1)]?this[b6(0x231)]=af[b6(0x1a7)]:b6(0x241)===af[b6(0x1f1)]?(this[b6(0x180)]=this[b6(0x1a7)]=af[b6(0x1a7)],this['method']='return',this['next']=b6(0x1e6)):b6(0x1a1)===af[b6(0x1f1)]&&ag&&(this['next']=ag),Y;},'finish':function(af){var b7=aB;for(var ag=this[b7(0x17e)]['length']-0x1;ag>=0x0;--ag){var ah=this[b7(0x17e)][ag];if(ah[b7(0x24d)]===af)return this[b7(0x25e)](ah[b7(0x249)],ah[b7(0x1b6)]),ab(ah),Y;}},'catch':function(af){var b8=aB;for(var ag=this['tryEntries'][b8(0x1ba)]-0x1;ag>=0x0;--ag){var ah=this[b8(0x17e)][ag];if(ah['tryLoc']===af){var ai=ah[b8(0x249)];if('throw'===ai['type']){var aj=ai[b8(0x1a7)];ab(ah);}return aj;}}throw Error(b8(0x1a3));},'delegateYield':function(af,ag,ah){var b9=aB;return this['delegate']={'iterator':ad(af),'resultName':ag,'nextLoc':ah},b9(0x231)===this[b9(0x1e9)]&&(this[b9(0x1a7)]=q),Y;}},z;}d['exports']=i,d[aA(0x18c)]['__esModule']=!0x0,d[aA(0x18c)][aA(0x218)]=d[aA(0x18c)];},0x1294:function(d,f,g){var ba=a7b,h=g(0x1219)();d['exports']=h;try{regeneratorRuntime=h;}catch(i){ba(0x24c)==typeof globalThis?globalThis[ba(0x1ad)]=h:Function('r','regeneratorRuntime\x20=\x20r')(h);}}},b={};function c(d){var bb=a7b,f=b[d];if(void 0x0!==f)return f[bb(0x18c)];var g=b[d]={'exports':{}};return a[d](g,g[bb(0x18c)],c),g['exports'];}c['n']=function(d){var bc=a7b,f=d&&d[bc(0x212)]?function(){return d['default'];}:function(){return d;};return c['d'](f,{'a':f}),f;},c['d']=function(d,f){var bd=a7b;for(var g in f)c['o'](f,g)&&!c['o'](d,g)&&Object[bd(0x203)](d,g,{'enumerable':!0x0,'get':f[g]});},c['o']=function(d,f){return Object['prototype']['hasOwnProperty']['call'](d,f);},(function(){'use strict';var bC=a7b;function q(ag,ah,ai,aj,ak,al,am){var be=a7b;try{var an=ag[al](am),ao=an[be(0x19c)];}catch(ap){return void ai(ap);}an['done']?ah(ao):Promise[be(0x244)](ao)[be(0x1e2)](aj,ak);}function z(ag){var bf=a7b;return z=bf(0x226)==typeof Symbol&&bf(0x1c7)==typeof Symbol[bf(0x1a9)]?function(ah){return typeof ah;}:function(ah){var bg=bf;return ah&&bg(0x226)==typeof Symbol&&ah['constructor']===Symbol&&ah!==Symbol[bg(0x22a)]?bg(0x1c7):typeof ah;},z(ag);}function A(ag){var bi=a7b,ah=function(ai,aj){var bh=a7b;if(bh(0x24c)!=z(ai)||!ai)return ai;var ak=ai[Symbol['toPrimitive']];if(void 0x0!==ak){var al=ak[bh(0x181)](ai,aj||bh(0x218));if(bh(0x24c)!=z(al))return al;throw new TypeError(bh(0x20f));}return(bh(0x1a6)===aj?String:Number)(ai);}(ag,bi(0x1a6));return bi(0x1c7)==z(ah)?ah:ah+'';}function B(ag,ah){var bj=a7b;for(var ai=0x0;ai<ah[bj(0x1ba)];ai++){var aj=ah[ai];aj[bj(0x219)]=aj[bj(0x219)]||!0x1,aj[bj(0x220)]=!0x0,'value'in aj&&(aj[bj(0x196)]=!0x0),Object[bj(0x203)](ag,A(aj[bj(0x23a)]),aj);}}function C(ag,ah,ai){var bk=a7b;return ah&&B(ag[bk(0x22a)],ah),ai&&B(ag,ai),Object['defineProperty'](ag,'prototype',{'writable':!0x1}),ag;}function D(ag,ah){var bl=a7b;if(!(ag instanceof ah))throw new TypeError(bl(0x186));}function F(ag,ah){var bm=a7b;if(ah&&(bm(0x24c)==z(ah)||'function'==typeof ah))return ah;if(void 0x0!==ah)throw new TypeError(bm(0x223));return function(ai){var bn=bm;if(void 0x0===ai)throw new ReferenceError(bn(0x23c));return ai;}(ag);}function G(ag){var bo=a7b;return G=Object['setPrototypeOf']?Object[bo(0x227)][bo(0x192)]():function(ah){var bp=bo;return ah[bp(0x200)]||Object[bp(0x227)](ah);},G(ag);}function H(ag,ah){return H=Object['setPrototypeOf']?Object['setPrototypeOf']['bind']():function(ai,aj){var bq=a7b;return ai[bq(0x200)]=aj,ai;},H(ag,ah);}function I(ag,ah){var br=a7b;if('function'!=typeof ah&&null!==ah)throw new TypeError(br(0x1fe));ag[br(0x22a)]=Object['create'](ah&&ah[br(0x22a)],{'constructor':{'value':ag,'writable':!0x0,'configurable':!0x0}}),Object[br(0x203)](ag,br(0x22a),{'writable':!0x1}),ah&&H(ag,ah);}var J=c(0x1294),K=c['n'](J);function Q(ag,ah,ai){var bt=a7b;(function(aj,ak){var bs=a7b;if(ak[bs(0x188)](aj))throw new TypeError(bs(0x1c8));}(ag,ah),ah[bt(0x22d)](ag,ai));}function U(ag,ah,ai){return ag['set'](X(ag,ah),ai),ai;}function V(ag,ah){return ag['get'](X(ag,ah));}function X(ag,ah,ai){var bu=a7b;if('function'==typeof ag?ag===ah:ag['has'](ah))return arguments[bu(0x1ba)]<0x3?ah:ai;throw new TypeError(bu(0x239));}var Y=new WeakMap(),Z=new WeakMap(),a0=new WeakMap(),a1=new WeakMap(),a2=new WeakMap(),a3=(function(){var bw=a7b;function ag(ah){var bv=a7b;if(D(this,ag),Q(this,Y,!0x1),Q(this,Z,void 0x0),Q(this,a0,void 0x0),Q(this,a1,void 0x0),Q(this,a2,void 0x0),0x1!==arguments[bv(0x1ba)])throw new TypeError(bv(0x230));bv(0x190)===ah?(U(Y,this,!0x0),ah=bv(0x255)):bv(0x1a6)!=typeof ah&&(ah=ah[bv(0x216)]());var ai=a7['_']['exec'](ah);if(!ai)throw new SyntaxError(bv(0x209)+ah);if(U(Z,this,ai[0x1]||'file'),U(a0,this,ai[0x2]||''),U(a1,this,ai[0x3]),bv(0x1b5)!==V(Z,this)&&!V(a0,this))throw new SyntaxError(bv(0x209)+ah);U(a2,this,this['XtoRegExp']());}return C(ag,[{'key':bw(0x20a),'value':function(){var bx=bw,ah='^';V(Y,this)?ah+=bx(0x1ec):'*'===V(Z,this)?ah+='https?://<host>':ah+=V(Z,this)+bx(0x205),ah=(ah+=bx(0x208))[bx(0x247)]('<scheme>',a4['_']);var ai='';if(V(a0,this)){if('*'===V(a0,this))ai=bx(0x213);else{var aj=V(a0,this);V(a0,this)[bx(0x189)]('*.')&&(ai=bx(0x1d1),aj=V(a0,this)['substr'](0x2)),ai+=aj[bx(0x247)](/[.-]/g,bx(0x18a));}ai=ai[bx(0x247)](/<label>/g,a5['_']);}return ah=(ah=ah[bx(0x247)]('<host>',ai))[bx(0x247)](bx(0x1bf),V(a1,this)[bx(0x247)](/[.+?^${}()|[\]\\\/]/g,'\x5c$&')[bx(0x247)](/\*/g,'.*')),new RegExp(ah);}},{'key':bw(0x1b3),'value':function(){return V(Y,this)?'<all_urls>':V(a0,this)?{'scheme':V(Z,this),'host':V(a0,this),'path':V(a1,this)}:{'scheme':V(Z,this),'path':V(a1,this)};}},{'key':bw(0x216),'value':function(){var by=bw;return V(Y,this)?by(0x190):V(Z,this)+'://'+V(a0,this)+V(a1,this);}},{'key':bw(0x25d),'value':function(){var bz=bw,ah=arguments[bz(0x1ba)]>0x0&&void 0x0!==arguments[0x0]?arguments[0x0]:'';return new RegExp(V(a2,this),ah);}},{'key':bw(0x1df),'value':function(ah){var bA=bw;if(!(ah instanceof URL))try{ah=new URL(ah);}catch(ai){return!0x1;}return ah=''[bA(0x217)](ah[bA(0x1f5)],'//')[bA(0x217)](ah['hostname'])[bA(0x217)](ah['pathname'])[bA(0x217)](ah['search']),V(a2,this)['test'](ah);}}],[{'key':'test','value':function(ah){var bB=bw,ai=arguments[bB(0x1ba)]>0x1&&void 0x0!==arguments[0x1]?arguments[0x1]:'';switch(arguments['length']){case 0x1:if(ah instanceof ag)return!0x0;try{return new ag(ah),!0x0;}catch(aj){return!0x1;}case 0x2:try{return ah instanceof ag||(ah=new ag(ah)),ah['test'](ai);}catch(ak){return!0x1;}default:throw new TypeError(bB(0x21b));}}},{'key':bw(0x25d),'value':function(ah,ai){return(ah=new ag(ah))['toRegExp'](ai);}}]);}()),a4={'_':'https?|wss?|ftp|file'},a5={'_':bC(0x1e7)},a6={'_':bC(0x1b8)['replace'](bC(0x20b),a4['_'])['replace'](bC(0x210),'\x5c*|(?:\x5c*\x5c.)?<label>(?:\x5c.<label>)*'['replace'](/<label>/g,a5['_']))[bC(0x247)](bC(0x1bf),bC(0x243))},a7={'_':new RegExp(a6['_'])},a8=a3;function a9(ag,ah,ai){var bD=bC;return ah=G(ah),F(ag,aa()?Reflect[bD(0x253)](ah,ai||[],G(ag)[bD(0x1e1)]):ah[bD(0x252)](ag,ai));}function aa(){var bE=bC;try{var ag=!Boolean[bE(0x22a)][bE(0x1b3)][bE(0x181)](Reflect[bE(0x253)](Boolean,[],function(){}));}catch(ah){}return(aa=function(){return!!ag;})();}var ab=localStorage[bC(0x182)]('lvsPlatformConfig'),ac=ab?JSON[bC(0x215)](ab):{'chaturbate':{'isRoomPage':[bC(0x18d),bC(0x228),'*://*.chaturbate.eu/b/*','*://*.chaturbate.la/b/*','*://*.chaturbate.me/b/*',bC(0x1af),bC(0x1ff),'*://*.chaturbatefreecams.com/b/*',bC(0x21f),bC(0x1f8),bC(0x23f),bC(0x18b),'*://*.cb.dev/b/*','*://*.cams.homelivesex.com/b/*','*://*.camru.top/b/*','*://*.camdudes.com/b/*','*://chaturbate.jjgirls.com/b/*','*://*.webmodels.live/b/*',bC(0x1b2),'*://*.ru2.camru.top/b/*',bC(0x206),'*://*.camangels.com/b/*','*://*.adultcams.su/b/*',bC(0x1b4),'*://*.lolycam.com/b/*','*://*.camscaster.com/b/*',bC(0x1e3),bC(0x1c6),bC(0x23d),bC(0x259),bC(0x1d3),bC(0x201),'*://*.cams.nudelive.com/b/*',bC(0x17c),bC(0x191),bC(0x229),bC(0x238),bC(0x1ee),bC(0x233),bC(0x240),bC(0x1cc),bC(0x1d2),'*://*.chaturbate.wang/b/*',bC(0x17d),'*://*.chinesecamsplus.com/b/*',bC(0x242)]},'bongacams':{'isRoomPage':[bC(0x1d9),'*://*.bongamodels2.com/chat-console*',bC(0x245),bC(0x197),bC(0x251)]},'cam4':{'isRoomPage':[bC(0x18e),bC(0x232),bC(0x22c),bC(0x1e5)]},'myfreecams':{'isRoomPage':['*://*.myfreecams.com/modelweb/*']},'camsoda':{'isRoomPage':[bC(0x1d4),bC(0x1eb),bC(0x1ac)]},'stripchat':{'isRoomPage':[bC(0x1c3),bC(0x221),bC(0x19d),'*://*.stripchat.dev/*',bC(0x1c2),'*://*.stripdev.com/*',bC(0x236),'*://*.strip.chat/*',bC(0x1f3),'*://*.stripchat3.com/*','*://*.xlivesex.com/*',bC(0x1ab),bC(0x1d7),bC(0x1e8)]}},ad={},ae={};function af(ag,ah){try{return a8['test'](ag,ah);}catch(ai){return!0x1;}}!function(ag,ah){var bF=bC,ai='',aj=location[bF(0x254)];for(var ak in ac){if(ac[ak]['isRoomPage'][bF(0x24b)](function(al){return af(al,aj);})){ai=bF(0x184)===ak&&/[^]*.cb.dev\/[^]*/g['test'](aj)?bF(0x257):bF(0x1d0)===ak?bF(0x211):ak,ah(ag);break;}}ag[bF(0x22e)](bF(0x1f7),function(){var bG=bF;ag[bG(0x1d5)](new CustomEvent(bG(0x23e),{'bubbles':!0x0,'cancelable':!0x1,'detail':ai}));}),ag[bF(0x22e)](bF(0x225),function(al){var bH=bF,am,an=al[bH(0x19f)]||{},ao=an[bH(0x1ae)],ap=void 0x0===ao?'':ao,aq=an[bH(0x20e)],ar=void 0x0===aq?'':aq;ad[ap]&&(null===(am=ad[ap])||void 0x0===am||am[bH(0x198)](ar));}),ag[bF(0x22e)](bF(0x1b1),function(al){var bI=bF,am,an=al[bI(0x19f)]||{},ao=an['url'],ap=void 0x0===ao?'':ao,aq=an['data'],ar=void 0x0===aq?'':aq;ae[ap]&&(null===(am=ae[ap])||void 0x0===am||am['postMessage'](ar));});}(window,function(ag){var bJ=bC;ag[bJ(0x199)]=function(ai){function aj(){var bK=a7b,ak;D(this,aj);for(var al=arguments[bK(0x1ba)],am=new Array(al),an=0x0;an<al;an++)am[an]=arguments[an];return(ak=a9(this,aj,[][bK(0x217)](am)))['addEventListener']('open',function(ao){var bL=bK;ad[ak[bL(0x1ae)]]=ak;}),ak[bK(0x22e)](bK(0x1d8),function(ao){delete ad[ak['url']];}),ak['addEventListener'](bK(0x19a),function(ao){var bM=bK,ap,aq={'data':null==ao?void 0x0:ao[bM(0x20e)],'url':null==ao||null===(ap=ao[bM(0x235)])||void 0x0===ap?void 0x0:ap[bM(0x1ae)]};ag[bM(0x1d5)](new CustomEvent(bM(0x1fc),{'bubbles':!0x0,'cancelable':!0x1,'detail':JSON[bM(0x1a4)](aq)}));}),ak;}return I(aj,ai),C(aj);}(ag[bJ(0x199)]),ag[bJ(0x24e)]=function(ai){function aj(){var bN=a7b,ak;D(this,aj);for(var al=arguments[bN(0x1ba)],am=new Array(al),an=0x0;an<al;an++)am[an]=arguments[an];return(ak=a9(this,aj,[][bN(0x217)](am)))[bN(0x22e)](bN(0x1a8),function(){var bO=bN;if(0x4===ak['readyState']&&bO(0x1cf)!==ak[bO(0x1fd)]){var ao;ao={'url':ak[bO(0x183)]||window['location'][bO(0x250)],'data':{'readyState':ak[bO(0x1b7)],'response':ak[bO(0x1ce)],'responseURL':ak[bO(0x183)],'responseType':ak[bO(0x1fd)],'status':ak[bO(0x18f)],'statusText':ak[bO(0x1c5)]}},ag[bO(0x1d5)](new CustomEvent('LVS_XHR_MESSAGE',{'bubbles':!0x0,'cancelable':!0x1,'detail':JSON[bO(0x1a4)](ao)}));}},!0x1),ak;}return I(aj,ai),C(aj);}(ag[bJ(0x24e)]);var ah=ag[bJ(0x1d6)];ag['fetch']=function(){var bP=bJ,ai=new URL(arguments['length']<=0x0?void 0x0:arguments[0x0],window[bP(0x21d)][bP(0x250)]),aj=ai[bP(0x1ae)],ak=ai[bP(0x254)];return aj||(aj=ak),ah['apply'](void 0x0,arguments)['then']((function(){var bQ=bP,al,am=(al=K()[bQ(0x237)](function an(ao){var bR=bQ,ap,aq,ar,as,at;return K()[bR(0x1b0)](function(au){var bS=bR;for(;;)switch(au[bS(0x1aa)]=au[bS(0x231)]){case 0x0:if(aj){au[bS(0x231)]=0x2;break;}return au[bS(0x1cd)]('return',ao);case 0x2:if(!((ap=ao[bS(0x1bc)]())instanceof Response)){au[bS(0x231)]=0x1e;break;}return aq=void 0x0,ar='',as='',au['prev']=0x7,au['next']=0xa,ao[bS(0x1bc)]()[bS(0x24f)]();case 0xa:aq=au[bS(0x1ca)],as=bS(0x24f),au[bS(0x231)]=0x11;break;case 0xe:au[bS(0x1aa)]=0xe,au['t0']=au[bS(0x214)](0x7);case 0x11:if(aq){au[bS(0x231)]=0x1c;break;}return au[bS(0x1aa)]=0x12,au['next']=0x15,ao['clone']()[bS(0x1e0)]();case 0x15:ar=au[bS(0x1ca)],as=bS(0x1e0),au[bS(0x231)]=0x1c;break;case 0x19:au[bS(0x1aa)]=0x19,au['t1']=au[bS(0x214)](0x12);case 0x1c:at={'url':aj,'data':{'readyState':0x4,'response':aq||ar,'responseURL':aj,'responseType':as,'status':ap['status'],'statusText':ap[bS(0x1c5)]}},ag['dispatchEvent'](new CustomEvent(bS(0x258),{'bubbles':!0x0,'cancelable':!0x1,'detail':JSON[bS(0x1a4)](at)}));case 0x1e:return au[bS(0x1cd)](bS(0x241),ao);case 0x1f:case bS(0x1e6):return au[bS(0x1bb)]();}},an,null,[[0x7,0xe],[0x12,0x19]]);}),function(){var ao=this,ap=arguments;return new Promise(function(aq,ar){var bT=a7b,as=al[bT(0x252)](ao,ap);function at(av){q(as,aq,ar,at,au,'next',av);}function au(av){q(as,aq,ar,at,au,'throw',av);}at(void 0x0);});});return function(ao){var bU=bQ;return am[bU(0x252)](this,arguments);};}()));},ag[bJ(0x1dc)]=function(ai){function aj(){var bV=a7b;for(var ak,al=arguments['length'],am=new Array(al),an=0x0;an<al;an++)am[an]=arguments[an];return D(this,aj),(ak=a9(this,aj,[][bV(0x217)](am)))[bV(0x22e)]('message',function(ao){var bW=bV,ap=JSON[bW(0x215)](JSON[bW(0x1a4)](new URL(am[0x0],window[bW(0x21d)]['origin'])));ae[ap]=ak,ag[bW(0x1d5)](new CustomEvent(bW(0x256),{'bubbles':!0x0,'cancelable':!0x1,'detail':{'url':ap,'data':null==ao?void 0x0:ao[bW(0x20e)]}}));}),ak;}return I(aj,ai),C(aj);}(ag['Worker']),bJ(0x1ed)in navigator&&navigator[bJ(0x1ed)][bJ(0x22e)](bJ(0x19a),function(ai){var bX=bJ;ag[bX(0x1d5)](new CustomEvent(bX(0x194),{'bubbles':!0x0,'cancelable':!0x1,'detail':ai}));});});}());}()));function a7a(){var bY=['Invalid\x20URL\x20match\x20pattern:\x20','XtoRegExp','<scheme>','_invoke','done','data','@@toPrimitive\x20must\x20return\x20a\x20primitive\x20value.','<host>','bongamodels','__esModule','(<label>\x5c.)*<label>','catch','parse','toString','concat','default','enumerable','forEach','URLMatchPattern.test\x20requires\x201\x20or\x202\x20arguments','create','location','The\x20iterator\x20does\x20not\x20provide\x20a\x20\x27','*://*.latinchaturbate.com/b/*','configurable','*://*.stripchat.com/cams/*','\x20is\x20not\x20iterable','Derived\x20constructors\x20may\x20only\x20return\x20object\x20or\x20undefined','2534502xVVIld','SEND_LVS_MESSAGE_BY_WEB_SOCKET','function','getPrototypeOf','*://*.chaturbate.com/b/*','*://chaturbate.megacams.me/b/*','prototype','suspendedStart','*://*.cam4.es/broadcast*','set','addEventListener','isGeneratorFunction','URLMatchPattern\x20requires\x20exact\x201\x20argument','next','*://*.cam4.fr/broadcast*','*://*.cams69.net/b/*','setPrototypeOf','target','*://*.xhamsterlive.com/*','mark','*://*.cam2it.com/b/*','Private\x20element\x20is\x20not\x20present\x20on\x20this\x20object','key','__await','this\x20hasn\x27t\x20been\x20initialised\x20-\x20super()\x20hasn\x27t\x20been\x20called','*://*.homeoffice.live/b/*','LVS_SITE_KEY','*://*.camvirt.com/b/*','*://*.chatur.cc/b/*','return','*://*.chatbang.net/b/*','/.*','resolve','*://*.bongamodels.com/console*','\x27\x20method','replace','Generator\x20is\x20already\x20running','completion','pop','some','object','finallyLoc','XMLHttpRequest','json','origin','*://*.camvoltmodels.com/console*','apply','construct','href','*://*/*','LVS_WORKER_MESSAGE','testbed','LVS_XHR_MESSAGE','*://*.pastabate.com/b/*','catchLoc','[object\x20Generator]','28SBjLgC','toRegExp','complete','*://*.freecams.me/b/*','*://*.chaturbate.su/b/*','tryEntries','name','rval','call','getItem','responseURL','chaturbate','continue','Cannot\x20call\x20a\x20class\x20as\x20a\x20function','async','has','startsWith','\x5c$&','*://*.cht.xxx/*','exports','*://*.chaturbate.asia/b/*','*://*.cam4.com/broadcast*','status','<all_urls>','*://cams.gaypage.com/b/*','bind','asyncIterator','LVS_SERVICE_WORKER_MESSAGE','17930hEQAaQ','writable','*://*.bongamodels2.com/console*','send','WebSocket','message','890QltVjQ','value','*://*.stripchat.com/*','265112CvhUDN','detail','suspendedYield','normal','Generator','illegal\x20catch\x20attempt','stringify','root','string','arg','readystatechange','iterator','prev','*://*.stripchat.global/*','*://*.cumchater.com/*','regeneratorRuntime','url','*://*.chatubate.me/b/*','wrap','SEND_LVS_MESSAGE_BY_WORKER','*://*.chaturbatecams.com/b/*','valueOf','*://*.cams.naughtyads.com.au/b/*','file','afterLoc','readyState','^<all_urls>$|^(?:(?:file://|(?:(\x5c*|<scheme>)://(<host>)))(<path>))$','push','length','stop','clone','dispatchException','charAt','<path>','nextLoc','129771bmOhAM','*://*.mywebcamroom.com/*','*://stripchat.com/cams/*','throw','statusText','*://*.thecarnal.cafe/b/*','symbol','Cannot\x20initialize\x20the\x20same\x20private\x20elements\x20twice\x20on\x20an\x20object','completed','sent','values','*://*.chatyourbate.com/b/*','abrupt','response','arraybuffer','bongacams','(<label>\x5c.)*','*://*.chaterbate.cc/b/*','*://*.nudes.show/b/*','*://*.camsoda.com/*','dispatchEvent','fetch','*://*.superchat.live/*','close','*://*.bongamodels.com/chat-console*','displayName','toStringTag','Worker','2490425IenWYW','hasOwnProperty','test','text','constructor','then','*://*.my.freecams6.com/b/*','executing','*://*.cam4.de.com/broadcast*','end','[0-9A-Za-z]+(?:\x5c-+[0-9A-Za-z]+)*','*://*.xham.live/*','method','awrap','*://*.elitemodelsnow.net/*','(file://|(<scheme>)://<host>)','serviceWorker','*://chat.chaturbate.lu/b/*','1YUNgjB','1572594DZVfYA','type','break','*://*.livelesbiansexcams.com/*','tryLoc','protocol','1830048JGUwYk','GET_LVS_SITE_KEY','*://*.privatecams.com/b/*','GeneratorFunction','@@iterator','keys','LVS_WEB_SOCKET_MESSAGE','responseType','Super\x20expression\x20must\x20either\x20be\x20null\x20or\x20a\x20function','*://*.chaturbate.global/b/*','__proto__','*://*.facetimegirls.com/b/*','_sent','defineProperty','delegate','://<host>','*://*.camgirllover.com/b/*','slice','<path>$'];a7a=function(){return bY;};return a7a();}
\ No newline at end of file
const a8h=a8b;(function(a,b){const g=a8b,c=a();while(!![]){try{const d=-parseInt(g(0x125))/0x1*(parseInt(g(0x123))/0x2)+-parseInt(g(0x12a))/0x3*(-parseInt(g(0x120))/0x4)+-parseInt(g(0x121))/0x5+parseInt(g(0x126))/0x6*(parseInt(g(0x11d))/0x7)+-parseInt(g(0x11f))/0x8+parseInt(g(0x127))/0x9+parseInt(g(0x11b))/0xa;if(d===b)break;else c['push'](c['shift']());}catch(f){c['push'](c['shift']());}}}(a8a,0x8a6d6));let headElem=document[a8h(0x124)](a8h(0x11c));function createScriptElement(a){const i=a8h,b=document['createElement'](i(0x11e));return b['type']=i(0x12b),b['src']=a,b;}function a8b(a,b){const c=a8a();return a8b=function(d,e){d=d-0x119;let f=c[d];return f;},a8b(a,b);}function a8a(){const j=['3068308PjmJiW','1528480UBcMDk','documentElement','50304rKbpGS','createElement','39gGGhbQ','882ahqxjn','2137041sYidJz','remove','runtime','3nEFAfQ','text/javascript','/js/intercept.js','appendChild','9156510OAUHdr','head','45626ajejhQ','script','8197608fPLyEN'];a8a=function(){return j;};return a8a();}try{headElem[a8h(0x11a)](createScriptElement(chrome[a8h(0x129)]['getURL'](a8h(0x119))));}catch(a8c){}document[a8h(0x122)][a8h(0x11a)](headElem),headElem[a8h(0x128)]();
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "Lovense Cam Extension",
"version": "31.5.6",
"description": "Have fun with your Lovense toys by customizing your own levels. ***Note: Remember to enable 'Allow user scripts' after installation. Click on 'Details' and turn on 'Allow user scripts'",
"author": "lovense",
"manifest_version": 3,
"minimum_chrome_version": "120",
"permissions": [
"offscreen",
"userScripts",
"storage",
"unlimitedStorage",
"desktopCapture"
],
"incognito": "split",
"icons": {
"16": "icons/icon-16.png",
"128": "icons/icon-128.png"
},
"options_page": "",
"action": {
"default_title": "Lovense Cam Extension",
"default_popup": "pages/chrome_popup.html"
},
"background": {
"service_worker": "js/service_worker.js"
},
"host_permissions": [
"<all_urls>"
],
"content_scripts": [
{
"js": [
"js/run-at-document-start.js"
],
"matches": [
"<all_urls>"
],
"exclude_matches": [
"*://*.lovense.com/cam-model/*",
"*://*.lovense.com/cam-model-v3/*",
"*://localhost/*"
],
"run_at": "document_start"
}
],
"content_security_policy": {
"extension_pages": "script-src 'self' http://localhost:*; object-src 'self';"
},
"externally_connectable": {
"matches": [
"*://*.lovense.com/cam-model/*",
"*://*.lovense.com/cam-model-v3/*",
"*://localhost/*"
]
},
"web_accessible_resources": [
{
"resources": [
"icons/*",
"img/*",
"js/*"
],
"matches": [
"<all_urls>"
]
}
],
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7zM8HealrzUCVVeJYzx5Vvz3VYwdWyfoQ5Vb7hP34PJvyJ9/5LnvzLoGHaUCjwSLHtT+IqFNkA6OUVhotpZzb4N1zaTq81QP0QnoOerHHQTqkiJyvtaVGiPhZlQ5dDhHoSLZJ1bNaltveGNG3CE2/yicohnJTlXmsh053trrQYVaR6HPJ5lICT37D7jsuMHayFjEsWTlZPLP6Wn80zrLEvpH93oA12UnWyQuPs5z/X5HhyrxzCmeJ4nZXtKuB6ICWU+dHSwzWm4N3l9qCxdvAjgsYwG64+34/i00E8eWgwISeOOVHV2p80UnOanQk7TBfiXJSBQgbosUbLZjmv7y6wIDAQAB"
}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>
Extension Background
</title>
<link rel="icon" href="/favicon.ico">
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<iframe id="backgroundIframe" width="100%" height="100%" frameborder="0" scrolling="no" allow="bluetooth;hid;serial"></iframe>
<script src="/js/chrome_background.js"></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>
Extension Popup
</title>
<link rel="icon" href="/favicon.ico">
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
body {
width: 400px;
height: 600px;
}
.popup-container {
width: 100%;
height: 100%;
}
.cam-page-loading {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
.loading-block {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
}
.loading-ball {
width: 20px;
height: 20px;
border-radius: 50%;
}
.loading-ball+.loading-ball {
margin-left: 20px;
}
.loading-ball:nth-child(1) {
background-color: #0a0258;
animation: leftBallLoad 1s infinite linear;
}
.loading-ball:nth-child(2) {
background-color: #ff2d89;
animation: rightBallLoad 1s infinite linear;
}
@keyframes leftBallLoad {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
25% {
transform: translate3d(20px, 0, 0) scale(1.3);
}
50% {
transform: translate3d(40px, 0, 0) scale(1);
}
75% {
transform: translate3d(20px, 0, 0) scale(0.7);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
@keyframes rightBallLoad {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
25% {
transform: translate3d(-20px, 0, 0) scale(0.7);
}
50% {
transform: translate3d(-40px, 0, 0) scale(1);
}
75% {
transform: translate3d(-20px, 0, 0) scale(1.3);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
</style>
</head>
<body>
<div class="popup-container">
<div class="cam-page-loading" id="camPageLoading">
<div class="loading-block">
<div class="loading-ball"></div>
<div class="loading-ball"></div>
</div>
</div>
</div>
<script src="/js/chrome_popup.js"></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>
Stream Master
</title>
<link rel="icon" href="/cam-model/favicon.ico">
<style>
body {
margin: 0;
overflow: hidden;
}
.cam-page-loading {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hidden-cam-page-loading {
display: none !important;
}
.loading-block {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
}
.loading-ball {
width: 20px;
height: 20px;
border-radius: 50%;
}
.loading-ball+.loading-ball {
margin-left: 20px;
}
.loading-ball:nth-child(1) {
background-color: #0a0258;
animation: leftBallLoad 1s infinite linear;
}
.loading-ball:nth-child(2) {
background-color: #ff2d89;
animation: rightBallLoad 1s infinite linear;
}
@keyframes leftBallLoad {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
25% {
transform: translate3d(20px, 0, 0) scale(1.3);
}
50% {
transform: translate3d(40px, 0, 0) scale(1);
}
75% {
transform: translate3d(20px, 0, 0) scale(0.7);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
@keyframes rightBallLoad {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
25% {
transform: translate3d(-20px, 0, 0) scale(0.7);
}
50% {
transform: translate3d(-40px, 0, 0) scale(1);
}
75% {
transform: translate3d(-20px, 0, 0) scale(1.3);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div class="cam-page-loading" id="camPageLoading">
<div class="loading-block">
<div class="loading-ball"></div>
<div class="loading-ball"></div>
</div>
</div>
<script src="/js/stream_master.js"></script>
</body>
</html>
// Log when the background script is loaded
console.log('Background script loaded');
// Function to check if chrome.runtime is initialized
function isChromeRuntimeReady() {
// Check for our global initialization flags first
if (typeof window.__qtWebChannelTransportReady === 'boolean' &&
window.__qtWebChannelTransportReady === true) {
return true;
}
// Check if the isChromeRuntimeInitialized function exists and use it
if (typeof window.isChromeRuntimeInitialized === 'function') {
return window.isChromeRuntimeInitialized();
}
// Fallback to basic checks
return typeof chrome !== 'undefined' &&
chrome.runtime &&
typeof chrome.runtime.id === 'string';
}
// Function to wait for chrome.runtime to be initialized
function waitForChromeRuntime(callback, maxAttempts = 100) {
let attempts = 0;
function checkRuntime() {
if (isChromeRuntimeReady()) {
console.log('chrome.runtime API is ready');
callback();
} else if (attempts < maxAttempts) {
attempts++;
// Log less frequently to reduce console spam
if (attempts === 1 || attempts % 10 === 0) {
console.log(`Waiting for chrome.runtime API to initialize (attempt ${attempts}/${maxAttempts})...`);
}
// Try to trigger initialization if we're past several attempts
if (attempts > 20 && attempts % 20 === 0) {
console.log('Attempting to trigger chrome.runtime initialization');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
setTimeout(checkRuntime, 100);
} else {
console.error('ERROR: chrome.runtime API failed to initialize after multiple attempts');
// Report initialization status if available
if (typeof window.reportInitializationStatus === 'function') {
window.reportInitializationStatus();
} else {
console.error('Initialization status:');
console.error(`- chrome defined: ${typeof chrome !== 'undefined'}`);
console.error(`- chrome.runtime defined: ${typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined'}`);
console.error(`- __qtWebChannelTransportReady: ${window.__qtWebChannelTransportReady}`);
}
}
}
checkRuntime();
}
// Add a listener for the chrome.runtime.initialized event
document.addEventListener('chrome.runtime.initialized', (event) => {
console.log(`chrome.runtime API initialized for extension: ${event.detail.extensionId}`);
initializeBackgroundFunctionality();
});
// Initialize all background functionality
function initializeBackgroundFunctionality() {
// Verify chrome.runtime.onMessage exists before adding listener
if (!chrome.runtime || !chrome.runtime.onMessage) {
console.error('chrome.runtime.onMessage is not available');
// Try to create it if it doesn't exist
if (chrome.runtime && !chrome.runtime.onMessage) {
console.log('Attempting to create chrome.runtime.onMessage');
chrome.runtime.onMessage = {
addListener: function(callback) {
console.log('Added listener to newly created onMessage');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
}
// Now try to add the message listener
try {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Background received message:', message);
console.log('Sender:', sender);
// Send a response back
setTimeout(() => {
sendResponse({
received: true,
from: 'background',
timestamp: Date.now(),
echo: message
});
}, 500); // Simulate some async processing
return true; // Keep the message channel open for async response
});
console.log('Message listener set up successfully in background');
} catch (e) {
console.error('Failed to add message listener in background:', e);
}
// Verify chrome.runtime.onConnect exists before adding listener
if (!chrome.runtime || !chrome.runtime.onConnect) {
console.error('chrome.runtime.onConnect is not available');
// Try to create it if it doesn't exist
if (chrome.runtime && !chrome.runtime.onConnect) {
console.log('Attempting to create chrome.runtime.onConnect');
chrome.runtime.onConnect = {
addListener: function(callback) {
console.log('Added listener to newly created onConnect');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
}
// Now try to add the connect listener
try {
chrome.runtime.onConnect.addListener((port) => {
console.log(`Port connected: ${port.name}`);
// Listen for messages on this port
port.onMessage.addListener((message) => {
console.log(`Port message received on ${port.name}:`, message);
// Send a response back through the port
port.postMessage({
received: true,
from: 'background',
timestamp: Date.now(),
echo: message
});
});
// Handle port disconnection
port.onDisconnect.addListener(() => {
console.log(`Port disconnected: ${port.name}`);
if (chrome.runtime.lastError) {
console.error('Port error:', chrome.runtime.lastError);
}
});
// Send an initial message to the port
port.postMessage({
action: 'connected',
from: 'background',
timestamp: Date.now()
});
});
console.log('Connect listener set up successfully in background');
} catch (e) {
console.error('Failed to add connect listener in background:', e);
}
// Start the ping interval
startPingInterval();
// Log extension information
logExtensionInfo();
}
// Send a message to the popup every 10 seconds if it's open
function startPingInterval() {
setInterval(() => {
chrome.runtime.sendMessage({
action: 'ping',
from: 'background',
timestamp: Date.now()
}, (response) => {
if (chrome.runtime.lastError) {
// This is normal if the popup is not open
console.log('Ping failed (popup probably closed):', chrome.runtime.lastError.message);
} else {
console.log('Ping response:', response);
}
});
}, 10000);
}
// Log information about the extension
function logExtensionInfo() {
console.log('Extension ID:', chrome.runtime.id);
try {
const manifest = chrome.runtime.getManifest();
console.log('Manifest:', manifest);
} catch (error) {
console.error('Error getting manifest:', error);
}
}
// Check if runtime is ready and initialize, or wait for it
if (isChromeRuntimeReady()) {
console.log('chrome.runtime API is already available, initializing background');
initializeBackgroundFunctionality();
} else {
console.log('Waiting for chrome.runtime API to initialize...');
// The initialization will happen when the chrome.runtime.initialized event fires
}
\ No newline at end of file
// Log when the content script is loaded
console.log('Runtime API Test Extension: Content script loaded');
// Function to check if chrome.runtime is initialized
function isChromeRuntimeReady() {
// Check for our global initialization flags first
if (typeof window.__qtWebChannelTransportReady === 'boolean' &&
window.__qtWebChannelTransportReady === true) {
return true;
}
// Check if the isChromeRuntimeInitialized function exists and use it
if (typeof window.isChromeRuntimeInitialized === 'function') {
return window.isChromeRuntimeInitialized();
}
// Fallback to basic checks
return typeof chrome !== 'undefined' &&
chrome.runtime &&
typeof chrome.runtime.id === 'string';
}
// Function to wait for chrome.runtime to be initialized
function waitForChromeRuntime(callback, maxAttempts = 100) {
let attempts = 0;
function checkRuntime() {
if (isChromeRuntimeReady()) {
console.log('chrome.runtime API is ready');
callback();
} else if (attempts < maxAttempts) {
attempts++;
// Log less frequently to reduce console spam
if (attempts === 1 || attempts % 10 === 0) {
console.log(`Waiting for chrome.runtime API to initialize (attempt ${attempts}/${maxAttempts})...`);
}
// Try to trigger initialization if we're past several attempts
if (attempts > 20 && attempts % 20 === 0) {
console.log('Attempting to trigger chrome.runtime initialization');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
setTimeout(checkRuntime, 100);
} else {
console.error('ERROR: chrome.runtime API failed to initialize after multiple attempts');
// Report initialization status if available
if (typeof window.reportInitializationStatus === 'function') {
window.reportInitializationStatus();
} else {
console.error('Initialization status:');
console.error(`- chrome defined: ${typeof chrome !== 'undefined'}`);
console.error(`- chrome.runtime defined: ${typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined'}`);
console.error(`- __qtWebChannelTransportReady: ${window.__qtWebChannelTransportReady}`);
}
}
}
checkRuntime();
}
// Function to create a floating control panel
function createControlPanel() {
// Check if panel already exists
if (document.getElementById('runtime-api-test-panel')) {
return;
}
// Create panel container
const panel = document.createElement('div');
panel.id = 'runtime-api-test-panel';
panel.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
font-family: Arial, sans-serif;
font-size: 12px;
z-index: 9999;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
`;
// Create header
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
`;
const title = document.createElement('h3');
title.textContent = 'Runtime API Test';
title.style.margin = '0';
const closeBtn = document.createElement('button');
closeBtn.textContent = 'X';
closeBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
font-weight: bold;
`;
closeBtn.onclick = () => panel.remove();
header.appendChild(title);
header.appendChild(closeBtn);
panel.appendChild(header);
// Create buttons
const createButton = (text, onClick) => {
const button = document.createElement('button');
button.textContent = text;
button.style.cssText = `
margin: 5px 0;
padding: 5px;
width: 100%;
cursor: pointer;
`;
button.onclick = onClick;
return button;
};
// Add buttons for testing runtime API
panel.appendChild(createButton('Send Message to Background', () => {
waitForChromeRuntime(() => {
chrome.runtime.sendMessage(
{ action: 'test', from: 'content', url: window.location.href },
response => {
logOutput(`Response: ${JSON.stringify(response)}`);
}
);
logOutput('Message sent to background');
});
}));
panel.appendChild(createButton('Connect to Background', () => {
waitForChromeRuntime(() => {
const port = chrome.runtime.connect({ name: 'content-port' });
logOutput('Connected to background with port: content-port');
port.onMessage.addListener(msg => {
logOutput(`Port message received: ${JSON.stringify(msg)}`);
});
port.postMessage({ action: 'hello', from: 'content', url: window.location.href });
logOutput('Message sent through port');
});
}));
panel.appendChild(createButton('Get Extension URL', () => {
waitForChromeRuntime(() => {
const url = chrome.runtime.getURL('popup.html');
logOutput(`Extension URL: ${url}`);
});
}));
// Create output area
const output = document.createElement('div');
output.id = 'runtime-api-test-output';
output.style.cssText = `
margin-top: 10px;
padding: 5px;
background-color: #fff;
border: 1px solid #ddd;
height: 100px;
overflow-y: auto;
font-family: monospace;
font-size: 11px;
white-space: pre-wrap;
`;
panel.appendChild(output);
// Add to page
document.body.appendChild(panel);
// Log function
window.logOutput = function(message) {
const outputDiv = document.getElementById('runtime-api-test-output');
if (outputDiv) {
const timestamp = new Date().toLocaleTimeString();
outputDiv.innerHTML += `[${timestamp}] ${message}\n`;
outputDiv.scrollTop = outputDiv.scrollHeight;
}
};
logOutput('Content script panel initialized');
// Check chrome.runtime API status
if (isChromeRuntimeReady()) {
logOutput('chrome.runtime API is already available');
} else {
logOutput('Waiting for chrome.runtime API to initialize...');
}
// Setup message listener
setupMessageListener();
}
// Setup message listener when runtime is ready
function setupMessageListener() {
waitForChromeRuntime(() => {
// Verify chrome.runtime.onMessage exists before adding listener
if (!chrome.runtime || !chrome.runtime.onMessage) {
console.error('chrome.runtime.onMessage is not available');
// Try to create it if it doesn't exist
if (chrome.runtime && !chrome.runtime.onMessage) {
console.log('Attempting to create chrome.runtime.onMessage');
chrome.runtime.onMessage = {
addListener: function(callback) {
console.log('Added listener to newly created onMessage');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
}
// Now try to add the listener
try {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Content script received message:', message);
if (document.getElementById('runtime-api-test-output')) {
logOutput(`Message from background: ${JSON.stringify(message)}`);
}
sendResponse({ received: true, from: 'content', url: window.location.href });
return true; // Keep the message channel open for async response
});
console.log('Message listener set up successfully');
} catch (e) {
console.error('Failed to add message listener:', e);
if (document.getElementById('runtime-api-test-output')) {
logOutput(`ERROR: Failed to add message listener: ${e.message}`);
}
}
});
}
// Add a listener for the chrome.runtime.initialized event
document.addEventListener('chrome.runtime.initialized', (event) => {
console.log(`chrome.runtime API initialized for extension: ${event.detail.extensionId}`);
if (document.getElementById('runtime-api-test-output')) {
logOutput(`chrome.runtime API initialized for extension: ${event.detail.extensionId}`);
}
});
// Wait for page to be fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createControlPanel);
} else {
createControlPanel();
}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Create Extension Icons</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
}
canvas {
border: 1px solid #ccc;
margin: 10px;
}
.icon-container {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
button {
margin: 5px;
padding: 5px 10px;
}
.instructions {
background-color: #f5f5f5;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1>Extension Icon Generator</h1>
<div class="instructions">
<h3>Instructions:</h3>
<ol>
<li>Click the "Generate Icons" button to create simple placeholder icons</li>
<li>Right-click on each canvas and select "Save Image As..."</li>
<li>Save each icon with the appropriate filename (icon16.png, icon48.png, icon128.png)</li>
<li>Place the saved icons in your extension directory</li>
</ol>
</div>
<button id="generateBtn">Generate Icons</button>
<div class="icon-container">
<h3>16x16 Icon</h3>
<canvas id="canvas16" width="16" height="16"></canvas>
<button id="download16">Download icon16.png</button>
</div>
<div class="icon-container">
<h3>48x48 Icon</h3>
<canvas id="canvas48" width="48" height="48"></canvas>
<button id="download48">Download icon48.png</button>
</div>
<div class="icon-container">
<h3>128x128 Icon</h3>
<canvas id="canvas128" width="128" height="128"></canvas>
<button id="download128">Download icon128.png</button>
</div>
<script>
function drawIcon(canvas, size) {
const ctx = canvas.getContext('2d');
// Clear canvas
ctx.clearRect(0, 0, size, size);
// Draw background
ctx.fillStyle = '#4285F4'; // Google blue
ctx.fillRect(0, 0, size, size);
// Draw border
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = Math.max(1, size / 16);
ctx.strokeRect(ctx.lineWidth/2, ctx.lineWidth/2, size - ctx.lineWidth, size - ctx.lineWidth);
// Draw "R" for Runtime
ctx.fillStyle = '#FFFFFF';
ctx.font = `bold ${Math.floor(size * 0.7)}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('R', size/2, size/2);
}
function setupDownload(canvasId, downloadId, filename) {
document.getElementById(downloadId).addEventListener('click', () => {
const canvas = document.getElementById(canvasId);
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = filename;
link.href = dataURL;
link.click();
});
}
document.getElementById('generateBtn').addEventListener('click', () => {
drawIcon(document.getElementById('canvas16'), 16);
drawIcon(document.getElementById('canvas48'), 48);
drawIcon(document.getElementById('canvas128'), 128);
});
setupDownload('canvas16', 'download16', 'icon16.png');
setupDownload('canvas48', 'download48', 'icon48.png');
setupDownload('canvas128', 'download128', 'icon128.png');
// Generate icons on page load
window.onload = () => {
document.getElementById('generateBtn').click();
};
</script>
</body>
</html>
\ No newline at end of file
{
"name": "Runtime API Test Extension",
"version": "1.0",
"manifest_version": 3,
"description": "A test extension for chrome.runtime API",
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}
],
"permissions": [
"storage"
],
"host_permissions": [
"<all_urls>"
]
}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Runtime API Test Extension</title>
<style>
body {
width: 300px;
padding: 10px;
font-family: Arial, sans-serif;
}
button {
margin: 5px 0;
padding: 5px 10px;
width: 100%;
}
#output {
margin-top: 10px;
padding: 10px;
border: 1px solid #ccc;
background-color: #f5f5f5;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
font-family: monospace;
}
</style>
</head>
<body>
<h2>Runtime API Test</h2>
<button id="getUrl">Test chrome.runtime.getURL()</button>
<button id="sendMessage">Test chrome.runtime.sendMessage()</button>
<button id="connect">Test chrome.runtime.connect()</button>
<button id="getManifest">Test chrome.runtime.getManifest()</button>
<button id="getBackgroundPage">Test chrome.runtime.getBackgroundPage()</button>
<button id="clearOutput">Clear Output</button>
<div id="output"></div>
<script src="popup.js"></script>
</body>
</html>
\ No newline at end of file
// Function to log output to the output div
function log(message) {
const output = document.getElementById('output');
const timestamp = new Date().toLocaleTimeString();
output.innerHTML += `[${timestamp}] ${message}\n`;
output.scrollTop = output.scrollHeight;
}
// Function to check if chrome.runtime is initialized
function isChromeRuntimeReady() {
// Check for our global initialization flags first
if (typeof window.__qtWebChannelTransportReady === 'boolean' &&
window.__qtWebChannelTransportReady === true) {
return true;
}
// Check if the isChromeRuntimeInitialized function exists and use it
if (typeof window.isChromeRuntimeInitialized === 'function') {
return window.isChromeRuntimeInitialized();
}
// Fallback to basic checks
return typeof chrome !== 'undefined' &&
chrome.runtime &&
typeof chrome.runtime.id === 'string';
}
// Function to wait for chrome.runtime to be initialized
function waitForChromeRuntime(callback, maxAttempts = 100) {
let attempts = 0;
function checkRuntime() {
if (isChromeRuntimeReady()) {
log('chrome.runtime API is ready');
callback();
} else if (attempts < maxAttempts) {
attempts++;
// Log less frequently to reduce console spam
if (attempts === 1 || attempts % 10 === 0) {
log(`Waiting for chrome.runtime API to initialize (attempt ${attempts}/${maxAttempts})...`);
}
// Try to trigger initialization if we're past several attempts
if (attempts > 20 && attempts % 20 === 0) {
log('Attempting to trigger chrome.runtime initialization');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
setTimeout(checkRuntime, 100);
} else {
log('ERROR: chrome.runtime API failed to initialize after multiple attempts');
// Report initialization status if available
if (typeof window.reportInitializationStatus === 'function') {
window.reportInitializationStatus();
} else {
log('Initialization status:');
log(`- chrome defined: ${typeof chrome !== 'undefined'}`);
log(`- chrome.runtime defined: ${typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined'}`);
log(`- __qtWebChannelTransportReady: ${window.__qtWebChannelTransportReady}`);
}
}
}
checkRuntime();
}
// Add a listener for the chrome.runtime.initialized event
document.addEventListener('chrome.runtime.initialized', (event) => {
log(`chrome.runtime API initialized for extension: ${event.detail.extensionId}`);
});
// Test chrome.runtime.getURL()
document.getElementById('getUrl').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
const url = chrome.runtime.getURL('popup.html');
log(`getURL result: ${url}`);
} catch (error) {
log(`getURL error: ${error.message}`);
}
});
});
// Test chrome.runtime.sendMessage()
document.getElementById('sendMessage').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
const message = { action: 'test', from: 'popup', timestamp: Date.now() };
chrome.runtime.sendMessage(message, (response) => {
if (chrome.runtime.lastError) {
log(`sendMessage error: ${chrome.runtime.lastError.message}`);
} else {
log(`sendMessage response: ${JSON.stringify(response)}`);
}
});
log(`sendMessage sent: ${JSON.stringify(message)}`);
} catch (error) {
log(`sendMessage error: ${error.message}`);
}
});
});
// Test chrome.runtime.connect()
document.getElementById('connect').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
const port = chrome.runtime.connect({ name: 'popup-port' });
log(`connect: Port created with name 'popup-port'`);
// Verify port.onMessage exists before adding listener
if (!port.onMessage) {
log('port.onMessage is not available');
// Try to create it if it doesn't exist
port.onMessage = {
addListener: function(callback) {
log('Added listener to newly created port.onMessage');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
// Now try to add the message listener
try {
port.onMessage.addListener((message) => {
log(`Port received message: ${JSON.stringify(message)}`);
});
log('Port message listener set up successfully');
} catch (e) {
log(`Failed to add port message listener: ${e.message}`);
}
// Verify port.onDisconnect exists before adding listener
if (!port.onDisconnect) {
log('port.onDisconnect is not available');
// Try to create it if it doesn't exist
port.onDisconnect = {
addListener: function(callback) {
log('Added listener to newly created port.onDisconnect');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
// Now try to add the disconnect listener
try {
port.onDisconnect.addListener(() => {
log(`Port disconnected`);
});
log('Port disconnect listener set up successfully');
} catch (e) {
log(`Failed to add port disconnect listener: ${e.message}`);
}
// Try to send a message through the port
try {
port.postMessage({ action: 'hello', from: 'popup', timestamp: Date.now() });
log(`Port sent message`);
} catch (e) {
log(`Failed to send message through port: ${e.message}`);
}
} catch (error) {
log(`connect error: ${error.message}`);
}
});
});
// Test chrome.runtime.getManifest()
document.getElementById('getManifest').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
const manifest = chrome.runtime.getManifest();
log(`getManifest result: ${JSON.stringify(manifest, null, 2)}`);
} catch (error) {
log(`getManifest error: ${error.message}`);
}
});
});
// Test chrome.runtime.getBackgroundPage()
document.getElementById('getBackgroundPage').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
chrome.runtime.getBackgroundPage((backgroundPage) => {
if (chrome.runtime.lastError) {
log(`getBackgroundPage error: ${chrome.runtime.lastError.message}`);
} else {
log(`getBackgroundPage success: ${backgroundPage ? 'Background page accessed' : 'null'}`);
}
});
} catch (error) {
log(`getBackgroundPage error: ${error.message}`);
}
});
});
// Clear output
document.getElementById('clearOutput').addEventListener('click', () => {
document.getElementById('output').innerHTML = '';
});
// Setup message listener when runtime is ready
function setupMessageListener() {
waitForChromeRuntime(() => {
// Verify chrome.runtime.onMessage exists before adding listener
if (!chrome.runtime || !chrome.runtime.onMessage) {
log('chrome.runtime.onMessage is not available');
// Try to create it if it doesn't exist
if (chrome.runtime && !chrome.runtime.onMessage) {
log('Attempting to create chrome.runtime.onMessage');
chrome.runtime.onMessage = {
addListener: function(callback) {
log('Added listener to newly created onMessage');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
}
// Now try to add the listener
try {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
log(`Received message from background: ${JSON.stringify(message)}`);
sendResponse({ received: true, from: 'popup' });
return true; // Keep the message channel open for async response
});
log('Message listener set up successfully');
} catch (e) {
log(`Failed to add message listener: ${e.message}`);
}
});
}
// Log when popup is loaded
document.addEventListener('DOMContentLoaded', () => {
log('Popup loaded. Click buttons to test chrome.runtime API.');
// Check chrome.runtime API status
if (isChromeRuntimeReady()) {
log('chrome.runtime API is already available');
} else {
log('Waiting for chrome.runtime API to initialize...');
}
// Setup message listener
setupMessageListener();
});
\ No newline at end of file
console.log('BACKGROUND STARTED');
var running = false;
// Function to get data from chrome.storage
function getDataFromStorage(key) {
return new Promise((resolve, reject) => {
chrome.storage.local.get(key, (result) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(result);
}
});
});
}
function stopRunning() {
running = false;
chrome.runtime.sendMessage({message: "stop"});
}
function startRunning() {
running = true;
chrome.runtime.sendMessage({message: "start"});
}
function updateRunning(msg) {
chrome.runtime.sendMessage({message: "start", state: msg});
}
async function initializeXHAM() {
const randomString = Math.random().toString(36).substring(2, 18);
const url = "https://xhamsterlive.com/api/front/v3/config/initial?timezoneOffset=-120&timezone=Africa%2FJohannesburg&skipTimezoneAutoSaving=true&requestPath=%2Fearnings%2Fpaying-users&updateTag=0&disableClient=0&uniq="+randomString;
return fetch(url);
}
async function fetchUserData(userID) {
const randomString2 = Math.random().toString(36).substring(2, 18);
const userurl = "https://xhamsterlive.com/api/front/v2/users/"+userID+"?uniq="+randomString2;
return fetch(userurl);
}
// Function to fetch data from the API
async function requestTippersFriends() {
startRunning();
updateRunning("starting...");
var initdata;
try {
updateRunning("getting initialization data...")
const initresponse = await initializeXHAM()
if (!initresponse.ok) {
throw new Error(`HTTP error! status: ${initresponse.status}`);
}
initdata = await initresponse.json();
} catch (error) {
stopRunning();
console.log('INITDATA RETRIVIAL FAILED', error);
}
updateRunning("getting tipping users...");
const randomString = Math.random().toString(36).substring(2, 18);
const url = 'https://xhamsterlive.com/api/front/users/21483393/transactions/users?isOnline=&offset=0&limit=1000&username=&period=0&sort=lastPaid&order=desc&uniq='+randomString;
try {
const response = await fetch(url);
// Check if the response is ok (status in the range 200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data.users); // Log the result
updateRunning("get storage saved user list");
var friendedUsers = { 0: {id: 0, username:'fakeused', 'isDeleted': true}};
try {
const result = await getDataFromStorage('XhamsterLiveFriended');
console.log('Retrieved object:', result.XhamsterLiveFriended);
if(result.XhamsterLiveFriended) friendedUsers = result.XhamsterLiveFriended;
} catch (error) {
console.log('No object found, created:', friendedUsers);
}
console.log('FrienderUsers:', friendedUsers);
updateRunning("remove already known users");
const usersToFetch = [];
const existingUserIds = new Set(Object.values(friendedUsers).map(user => user.id));
Object.entries(data.users).forEach(([userKey, user]) => {
if ( user.isDeleted == false ) {
if (!existingUserIds.has(user.id)) {
console.log(`NEW USER: Key: ${userKey}, User ID: ${user.id}, Username: ${user.username}`);
usersToFetch.push(user.id);
}
}
});
console.log("users to Fetch", usersToFetch, usersToFetch.length);
if(usersToFetch.length < 1) {
stopRunning();
return;
}
const reqdata = { csrfToken: initdata.initial.client.csrfToken,
csrfTimestamp: initdata.initial.client.csrfTimestamp,
csrfNotifyTimestamp: initdata.initial.client.csrfNotifyTimestamp,
userIds: usersToFetch,
uniq: randomString }
try {
updateRunning("check which users we can friend");
const prefres = await fetch("https://xhamsterlive.com/api/front/models/21483393/preferences", {
method: 'POST', // Specify the request method
headers: {
'Content-Type': 'application/json' // Set the content type to JSON
},
body: JSON.stringify(reqdata) // Convert the request payload to a JSON string
});
if (!prefres.ok) {
throw new Error(`HTTP error! status: ${prefres.status}`);
}
const prefdata = await prefres.json()
console.log('prefdata', prefdata);
Object.entries(prefdata.canFriend).forEach(([userid, canFriend]) => {
if ( canFriend == false ) {
usersToFetch.pop(userid);
friendedUsers[userid] = data.users[userid];
}
});
console.log('userToFriend:', usersToFetch);
const rdata = {
csrfToken: initdata.initial.client.csrfToken,
csrfTimestamp: initdata.initial.client.csrfTimestamp,
csrfNotifyTimestamp: initdata.initial.client.csrfNotifyTimestamp,
uniq: randomString
}
var index=1;
updateRunning("sending friends requests: "+index+"/"+usersToFetch.length);
// Rate limiting variables
const maxConcurrentRequests = 1;
const maxRequestsPerInterval = 1;
const intervalDuration = 2000; // milliseconds
let currentRequests = 0;
let requestCount = 0;
// Function to handle the fetching with rate limiting
const fetchWithRateLimit = async (userId) => {
while (currentRequests >= maxConcurrentRequests) {
await new Promise(resolve => setTimeout(resolve, intervalDuration)); // Wait until a slot is free
}
currentRequests++;
try {
const uri = "https://xhamsterlive.com/api/front/users/"+initdata.initial.client.user.id+"/friends/"+userId;
console.log("sending friend request:", index+"/"+usersToFetch.length, uri, "with data", rdata);
const freq = await fetch(uri, {
method: "PUT",
headers: {
'Content-Type': 'application/json' // Set the content type to JSON
},
body: JSON.stringify(rdata)
});
if (!freq.ok) {
if(freq.status != "400") throw new Error(`HTTP error! status: ${freq.status}`);
}
if(freq.ok || freq.status=="400") friendedUsers[userId] = data.users[userId];
const udata = await freq.json();
console.log('friend requested for user:', '('+index+"/"+usersToFetch.length+")", udata);
index=index+1;
updateRunning("sending friends requests: "+index+"/"+usersToFetch.length);
} finally {
currentRequests--;
requestCount++;
// Reset the request count after the interval duration
if (requestCount >= maxRequestsPerInterval) {
await new Promise(resolve => setTimeout(resolve, intervalDuration));
requestCount = 0;
}
}
};
// Process users in batches
const promises = usersToFetch.map(userId => fetchWithRateLimit(userId));
await Promise.all(promises);
updateRunning("saving processed users");
console.log("FriendedUsers to store", friendedUsers);
chrome.storage.local.set({"XhamsterLiveFriended": friendedUsers});
} catch (error) {
stopRunning();
console.log('PREFENCE DATA RETRIVIAL FAILED', error);
}
} catch (error) {
stopRunning();
console.error('Error fetching data:', error);
}
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.message === "isrunning") {
sendResponse({isrunning: running});
}
if (request.message === "toggle") {
if (request.state === "run") {
console.log('RUN!!');
sendResponse({farewell: "Run!"});
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { message: "run" }, (response) => {
console.log(response); // Log the response from the content script
});
});
requestTippersFriends();
stopRunning();
}
}
});
function fuckSnap2()
{
document.querySelector('[title="View friend requests"]').click();
var btns = document.evaluate("//span[contains(., 'Accept')]", document, null, XPathResult.ANY_TYPE, null );
var btn = btns.iterateNext();
while(btn != null) {
btn.parentElement.parentElement.click();
btn = btns.iterateNext();
}
}
//setInterval(fuckSnap2, 5000);
//
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.message === "run") {
console.log('RUN!!');
sendResponse({ response: "Got it from content script!" });
}
});
console.log('LOADED!!!!');
{
"name":"XHamsterLive SHM extension",
"description":"Accept friends",
"version":"0.1.0",
"manifest_version":3,
"icons":{"16":"icon16.png","48":"icon48.png","128":"icon128.png"},
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "background.js"
},
"permissions": ["storage", "activeTab", "declarativeContent"],
"content_scripts":[
{
"matches":["https://xhamsterlive.com/*","http://xhamsterlive.com/*"],
"run_at":"document_end",
"js":["contentScript.js"]
}
]
}
<!DOCTYPE html>
<html>
<head>
<title>XhamsterLive SHM</title>
<style>
body {
width: 200px;
padding: 10px;
}
button {
width: 100%;
padding: 10px;
margin-top: 10px;
cursor: pointer;
}
</style>
</head>
<body>
<button id="run">Run</button>
<div id="runres"></div>
<script src="popup.js">
</script
</body>
</html>
console.log('POPUP.JS');
document.addEventListener('DOMContentLoaded', function() {
console.log('POPUP LOADED');
document.getElementById('run').addEventListener('click', function() {
chrome.runtime.sendMessage({message: "toggle", state: "run"}, function(response){
console.log(response.farewell);
}
);
console.log('PRESSED ON');
});
chrome.runtime.sendMessage({message: "isrunning"}, function(response){
console.log("isrunning?", response);
if(response.isrunning === true) document.getElementById("run").disabled = true;
else document.getElementById("run").disabled = false;
});
});
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.message == "update") {
document.getElementById('runres').innerHTML(request.state);
}
if (request.message == "start") {
document.getElementById("run").disabled = true;
}
if (request.message == "stop") {
document.getElementById("run").disabled = false;
}
});
# Chrome Extension Runtime API Emulation
This directory contains a test extension and implementation for emulating the Chrome Extension Runtime API in our Qt6-based browser.
## Overview
The implementation consists of:
1. **RuntimeBridge** (`assets/browser/js/runtime_bridge.py`): A Python QObject that bridges between the browser and JavaScript
2. **chrome-runtime-api.js** (`assets/browser/js/chrome-runtime-api.js`): JavaScript implementation of the chrome.runtime API
3. **Test Extension** (`assets/browser/extensions/test-extension/`): A simple extension to test the API implementation
## Supported APIs
The following chrome.runtime APIs are supported:
- `chrome.runtime.id` - Returns the extension ID
- `chrome.runtime.getURL()` - Converts a relative path to a fully-qualified extension URL
- `chrome.runtime.getManifest()` - Returns the extension's manifest.json contents
- `chrome.runtime.sendMessage()` - Sends a message to the extension or another extension
- `chrome.runtime.onMessage` - Event fired when a message is received
- `chrome.runtime.connect()` - Creates a connection to the extension or another extension
- `chrome.runtime.onConnect` - Event fired when a connection is made
- `chrome.runtime.getBackgroundPage()` - Returns the background page
- Various lifecycle events: `onInstalled`, `onStartup`, `onSuspend`, `onUpdateAvailable`
## Test Extension
The test extension demonstrates the use of the chrome.runtime API. It includes:
- **Popup** (`popup.html`, `popup.js`): A simple UI to test various chrome.runtime methods
- **Background Script** (`background.js`): A background script that listens for messages and connections
- **Content Script** (`content.js`): A content script that injects a control panel into web pages
## Testing
To test the chrome.runtime API implementation:
1. Run the test script: `python test_extension.py`
2. Click the "Test Extension" button in the toolbar
3. Use the buttons in the popup to test various chrome.runtime API methods
4. Check the browser console for log messages
You can also test the content script by navigating to any website and looking for the control panel in the bottom-right corner.
## Implementation Details
### Extension Loading
Extensions are loaded from the `assets/browser/extensions` directory and copied to the browser profile's extensions directory. The browser scans for extensions on startup and registers them with the runtime bridge.
### URL Scheme Handling
The browser uses a custom URL scheme handler (`qextension://`) to load extension resources. This allows extensions to use relative paths in their manifest and code.
### Message Passing
Message passing is implemented using QWebChannel to communicate between JavaScript and Python. The RuntimeBridge class handles routing messages between different parts of an extension.
### Background Pages
Background pages are loaded in hidden tabs. For service workers, a special HTML page is created that loads the service worker script.
## Limitations
- This is a simplified implementation and does not support all chrome.runtime APIs
- Service workers are not fully supported; they are loaded in a regular page context
- Cross-extension messaging is limited
- Some APIs may behave differently than in Chrome
\ No newline at end of file
// Log when the background script is loaded
console.log('Background script loaded');
// Function to check if chrome.runtime is initialized
function isChromeRuntimeReady() {
// Check for our global initialization flags first
if (typeof window.__qtWebChannelTransportReady === 'boolean' &&
window.__qtWebChannelTransportReady === true) {
return true;
}
// Check if the isChromeRuntimeInitialized function exists and use it
if (typeof window.isChromeRuntimeInitialized === 'function') {
return window.isChromeRuntimeInitialized();
}
// Fallback to basic checks
return typeof chrome !== 'undefined' &&
chrome.runtime &&
typeof chrome.runtime.id === 'string';
}
// Function to wait for chrome.runtime to be initialized
function waitForChromeRuntime(callback, maxAttempts = 100) {
let attempts = 0;
function checkRuntime() {
if (isChromeRuntimeReady()) {
console.log('chrome.runtime API is ready');
callback();
} else if (attempts < maxAttempts) {
attempts++;
// Log less frequently to reduce console spam
if (attempts === 1 || attempts % 10 === 0) {
console.log(`Waiting for chrome.runtime API to initialize (attempt ${attempts}/${maxAttempts})...`);
}
// Try to trigger initialization if we're past several attempts
if (attempts > 20 && attempts % 20 === 0) {
console.log('Attempting to trigger chrome.runtime initialization');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
setTimeout(checkRuntime, 100);
} else {
console.error('ERROR: chrome.runtime API failed to initialize after multiple attempts');
// Report initialization status if available
if (typeof window.reportInitializationStatus === 'function') {
window.reportInitializationStatus();
} else {
console.error('Initialization status:');
console.error(`- chrome defined: ${typeof chrome !== 'undefined'}`);
console.error(`- chrome.runtime defined: ${typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined'}`);
console.error(`- __qtWebChannelTransportReady: ${window.__qtWebChannelTransportReady}`);
}
}
}
checkRuntime();
}
// Add a listener for the chrome.runtime.initialized event
document.addEventListener('chrome.runtime.initialized', (event) => {
console.log(`chrome.runtime API initialized for extension: ${event.detail.extensionId}`);
initializeBackgroundFunctionality();
});
// Initialize all background functionality
function initializeBackgroundFunctionality() {
// Verify chrome.runtime.onMessage exists before adding listener
if (!chrome.runtime || !chrome.runtime.onMessage) {
console.error('chrome.runtime.onMessage is not available');
// Try to create it if it doesn't exist
if (chrome.runtime && !chrome.runtime.onMessage) {
console.log('Attempting to create chrome.runtime.onMessage');
chrome.runtime.onMessage = {
addListener: function(callback) {
console.log('Added listener to newly created onMessage');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
}
// Now try to add the message listener
try {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Background received message:', message);
console.log('Sender:', sender);
// Send a response back
setTimeout(() => {
sendResponse({
received: true,
from: 'background',
timestamp: Date.now(),
echo: message
});
}, 500); // Simulate some async processing
return true; // Keep the message channel open for async response
});
console.log('Message listener set up successfully in background');
} catch (e) {
console.error('Failed to add message listener in background:', e);
}
// Verify chrome.runtime.onConnect exists before adding listener
if (!chrome.runtime || !chrome.runtime.onConnect) {
console.error('chrome.runtime.onConnect is not available');
// Try to create it if it doesn't exist
if (chrome.runtime && !chrome.runtime.onConnect) {
console.log('Attempting to create chrome.runtime.onConnect');
chrome.runtime.onConnect = {
addListener: function(callback) {
console.log('Added listener to newly created onConnect');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
}
// Now try to add the connect listener
try {
chrome.runtime.onConnect.addListener((port) => {
console.log(`Port connected: ${port.name}`);
// Listen for messages on this port
port.onMessage.addListener((message) => {
console.log(`Port message received on ${port.name}:`, message);
// Send a response back through the port
port.postMessage({
received: true,
from: 'background',
timestamp: Date.now(),
echo: message
});
});
// Handle port disconnection
port.onDisconnect.addListener(() => {
console.log(`Port disconnected: ${port.name}`);
if (chrome.runtime.lastError) {
console.error('Port error:', chrome.runtime.lastError);
}
});
// Send an initial message to the port
port.postMessage({
action: 'connected',
from: 'background',
timestamp: Date.now()
});
});
console.log('Connect listener set up successfully in background');
} catch (e) {
console.error('Failed to add connect listener in background:', e);
}
// Start the ping interval
startPingInterval();
// Log extension information
logExtensionInfo();
}
// Send a message to the popup every 10 seconds if it's open
function startPingInterval() {
setInterval(() => {
chrome.runtime.sendMessage({
action: 'ping',
from: 'background',
timestamp: Date.now()
}, (response) => {
if (chrome.runtime.lastError) {
// This is normal if the popup is not open
console.log('Ping failed (popup probably closed):', chrome.runtime.lastError.message);
} else {
console.log('Ping response:', response);
}
});
}, 10000);
}
// Log information about the extension
function logExtensionInfo() {
console.log('Extension ID:', chrome.runtime.id);
try {
const manifest = chrome.runtime.getManifest();
console.log('Manifest:', manifest);
} catch (error) {
console.error('Error getting manifest:', error);
}
}
// Check if runtime is ready and initialize, or wait for it
if (isChromeRuntimeReady()) {
console.log('chrome.runtime API is already available, initializing background');
initializeBackgroundFunctionality();
} else {
console.log('Waiting for chrome.runtime API to initialize...');
// The initialization will happen when the chrome.runtime.initialized event fires
}
\ No newline at end of file
// Log when the content script is loaded
console.log('Runtime API Test Extension: Content script loaded');
// Function to check if chrome.runtime is initialized
function isChromeRuntimeReady() {
// Check for our global initialization flags first
if (typeof window.__qtWebChannelTransportReady === 'boolean' &&
window.__qtWebChannelTransportReady === true) {
return true;
}
// Check if the isChromeRuntimeInitialized function exists and use it
if (typeof window.isChromeRuntimeInitialized === 'function') {
return window.isChromeRuntimeInitialized();
}
// Fallback to basic checks
return typeof chrome !== 'undefined' &&
chrome.runtime &&
typeof chrome.runtime.id === 'string';
}
// Function to wait for chrome.runtime to be initialized
function waitForChromeRuntime(callback, maxAttempts = 100) {
let attempts = 0;
function checkRuntime() {
if (isChromeRuntimeReady()) {
console.log('chrome.runtime API is ready');
callback();
} else if (attempts < maxAttempts) {
attempts++;
// Log less frequently to reduce console spam
if (attempts === 1 || attempts % 10 === 0) {
console.log(`Waiting for chrome.runtime API to initialize (attempt ${attempts}/${maxAttempts})...`);
}
// Try to trigger initialization if we're past several attempts
if (attempts > 20 && attempts % 20 === 0) {
console.log('Attempting to trigger chrome.runtime initialization');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
setTimeout(checkRuntime, 100);
} else {
console.error('ERROR: chrome.runtime API failed to initialize after multiple attempts');
// Report initialization status if available
if (typeof window.reportInitializationStatus === 'function') {
window.reportInitializationStatus();
} else {
console.error('Initialization status:');
console.error(`- chrome defined: ${typeof chrome !== 'undefined'}`);
console.error(`- chrome.runtime defined: ${typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined'}`);
console.error(`- __qtWebChannelTransportReady: ${window.__qtWebChannelTransportReady}`);
}
}
}
checkRuntime();
}
// Function to create a floating control panel
function createControlPanel() {
// Check if panel already exists
if (document.getElementById('runtime-api-test-panel')) {
return;
}
// Create panel container
const panel = document.createElement('div');
panel.id = 'runtime-api-test-panel';
panel.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
font-family: Arial, sans-serif;
font-size: 12px;
z-index: 9999;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
`;
// Create header
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
`;
const title = document.createElement('h3');
title.textContent = 'Runtime API Test';
title.style.margin = '0';
const closeBtn = document.createElement('button');
closeBtn.textContent = 'X';
closeBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
font-weight: bold;
`;
closeBtn.onclick = () => panel.remove();
header.appendChild(title);
header.appendChild(closeBtn);
panel.appendChild(header);
// Create buttons
const createButton = (text, onClick) => {
const button = document.createElement('button');
button.textContent = text;
button.style.cssText = `
margin: 5px 0;
padding: 5px;
width: 100%;
cursor: pointer;
`;
button.onclick = onClick;
return button;
};
// Add buttons for testing runtime API
panel.appendChild(createButton('Send Message to Background', () => {
waitForChromeRuntime(() => {
chrome.runtime.sendMessage(
{ action: 'test', from: 'content', url: window.location.href },
response => {
logOutput(`Response: ${JSON.stringify(response)}`);
}
);
logOutput('Message sent to background');
});
}));
panel.appendChild(createButton('Connect to Background', () => {
waitForChromeRuntime(() => {
const port = chrome.runtime.connect({ name: 'content-port' });
logOutput('Connected to background with port: content-port');
port.onMessage.addListener(msg => {
logOutput(`Port message received: ${JSON.stringify(msg)}`);
});
port.postMessage({ action: 'hello', from: 'content', url: window.location.href });
logOutput('Message sent through port');
});
}));
panel.appendChild(createButton('Get Extension URL', () => {
waitForChromeRuntime(() => {
const url = chrome.runtime.getURL('popup.html');
logOutput(`Extension URL: ${url}`);
});
}));
// Create output area
const output = document.createElement('div');
output.id = 'runtime-api-test-output';
output.style.cssText = `
margin-top: 10px;
padding: 5px;
background-color: #fff;
border: 1px solid #ddd;
height: 100px;
overflow-y: auto;
font-family: monospace;
font-size: 11px;
white-space: pre-wrap;
`;
panel.appendChild(output);
// Add to page
document.body.appendChild(panel);
// Log function
window.logOutput = function(message) {
const outputDiv = document.getElementById('runtime-api-test-output');
if (outputDiv) {
const timestamp = new Date().toLocaleTimeString();
outputDiv.innerHTML += `[${timestamp}] ${message}\n`;
outputDiv.scrollTop = outputDiv.scrollHeight;
}
};
logOutput('Content script panel initialized');
// Check chrome.runtime API status
if (isChromeRuntimeReady()) {
logOutput('chrome.runtime API is already available');
} else {
logOutput('Waiting for chrome.runtime API to initialize...');
}
// Setup message listener
setupMessageListener();
}
// Setup message listener when runtime is ready
function setupMessageListener() {
waitForChromeRuntime(() => {
// Verify chrome.runtime.onMessage exists before adding listener
if (!chrome.runtime || !chrome.runtime.onMessage) {
console.error('chrome.runtime.onMessage is not available');
// Try to create it if it doesn't exist
if (chrome.runtime && !chrome.runtime.onMessage) {
console.log('Attempting to create chrome.runtime.onMessage');
chrome.runtime.onMessage = {
addListener: function(callback) {
console.log('Added listener to newly created onMessage');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
}
// Now try to add the listener
try {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Content script received message:', message);
if (document.getElementById('runtime-api-test-output')) {
logOutput(`Message from background: ${JSON.stringify(message)}`);
}
sendResponse({ received: true, from: 'content', url: window.location.href });
return true; // Keep the message channel open for async response
});
console.log('Message listener set up successfully');
} catch (e) {
console.error('Failed to add message listener:', e);
if (document.getElementById('runtime-api-test-output')) {
logOutput(`ERROR: Failed to add message listener: ${e.message}`);
}
}
});
}
// Add a listener for the chrome.runtime.initialized event
document.addEventListener('chrome.runtime.initialized', (event) => {
console.log(`chrome.runtime API initialized for extension: ${event.detail.extensionId}`);
if (document.getElementById('runtime-api-test-output')) {
logOutput(`chrome.runtime API initialized for extension: ${event.detail.extensionId}`);
}
});
// Wait for page to be fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createControlPanel);
} else {
createControlPanel();
}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Create Extension Icons</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
}
canvas {
border: 1px solid #ccc;
margin: 10px;
}
.icon-container {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
button {
margin: 5px;
padding: 5px 10px;
}
.instructions {
background-color: #f5f5f5;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1>Extension Icon Generator</h1>
<div class="instructions">
<h3>Instructions:</h3>
<ol>
<li>Click the "Generate Icons" button to create simple placeholder icons</li>
<li>Right-click on each canvas and select "Save Image As..."</li>
<li>Save each icon with the appropriate filename (icon16.png, icon48.png, icon128.png)</li>
<li>Place the saved icons in your extension directory</li>
</ol>
</div>
<button id="generateBtn">Generate Icons</button>
<div class="icon-container">
<h3>16x16 Icon</h3>
<canvas id="canvas16" width="16" height="16"></canvas>
<button id="download16">Download icon16.png</button>
</div>
<div class="icon-container">
<h3>48x48 Icon</h3>
<canvas id="canvas48" width="48" height="48"></canvas>
<button id="download48">Download icon48.png</button>
</div>
<div class="icon-container">
<h3>128x128 Icon</h3>
<canvas id="canvas128" width="128" height="128"></canvas>
<button id="download128">Download icon128.png</button>
</div>
<script>
function drawIcon(canvas, size) {
const ctx = canvas.getContext('2d');
// Clear canvas
ctx.clearRect(0, 0, size, size);
// Draw background
ctx.fillStyle = '#4285F4'; // Google blue
ctx.fillRect(0, 0, size, size);
// Draw border
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = Math.max(1, size / 16);
ctx.strokeRect(ctx.lineWidth/2, ctx.lineWidth/2, size - ctx.lineWidth, size - ctx.lineWidth);
// Draw "R" for Runtime
ctx.fillStyle = '#FFFFFF';
ctx.font = `bold ${Math.floor(size * 0.7)}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('R', size/2, size/2);
}
function setupDownload(canvasId, downloadId, filename) {
document.getElementById(downloadId).addEventListener('click', () => {
const canvas = document.getElementById(canvasId);
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = filename;
link.href = dataURL;
link.click();
});
}
document.getElementById('generateBtn').addEventListener('click', () => {
drawIcon(document.getElementById('canvas16'), 16);
drawIcon(document.getElementById('canvas48'), 48);
drawIcon(document.getElementById('canvas128'), 128);
});
setupDownload('canvas16', 'download16', 'icon16.png');
setupDownload('canvas48', 'download48', 'icon48.png');
setupDownload('canvas128', 'download128', 'icon128.png');
// Generate icons on page load
window.onload = () => {
document.getElementById('generateBtn').click();
};
</script>
</body>
</html>
\ No newline at end of file
{
"name": "Runtime API Test Extension",
"version": "1.0",
"manifest_version": 3,
"description": "A test extension for chrome.runtime API",
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}
],
"permissions": [
"storage"
],
"host_permissions": [
"<all_urls>"
]
}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Runtime API Test Extension</title>
<style>
body {
width: 300px;
padding: 10px;
font-family: Arial, sans-serif;
}
button {
margin: 5px 0;
padding: 5px 10px;
width: 100%;
}
#output {
margin-top: 10px;
padding: 10px;
border: 1px solid #ccc;
background-color: #f5f5f5;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
font-family: monospace;
}
</style>
</head>
<body>
<h2>Runtime API Test</h2>
<button id="getUrl">Test chrome.runtime.getURL()</button>
<button id="sendMessage">Test chrome.runtime.sendMessage()</button>
<button id="connect">Test chrome.runtime.connect()</button>
<button id="getManifest">Test chrome.runtime.getManifest()</button>
<button id="getBackgroundPage">Test chrome.runtime.getBackgroundPage()</button>
<button id="clearOutput">Clear Output</button>
<div id="output"></div>
<script src="popup.js"></script>
</body>
</html>
\ No newline at end of file
// Function to log output to the output div
function log(message) {
const output = document.getElementById('output');
const timestamp = new Date().toLocaleTimeString();
output.innerHTML += `[${timestamp}] ${message}\n`;
output.scrollTop = output.scrollHeight;
}
// Function to check if chrome.runtime is initialized
function isChromeRuntimeReady() {
// Check for our global initialization flags first
if (typeof window.__qtWebChannelTransportReady === 'boolean' &&
window.__qtWebChannelTransportReady === true) {
return true;
}
// Check if the isChromeRuntimeInitialized function exists and use it
if (typeof window.isChromeRuntimeInitialized === 'function') {
return window.isChromeRuntimeInitialized();
}
// Fallback to basic checks
return typeof chrome !== 'undefined' &&
chrome.runtime &&
typeof chrome.runtime.id === 'string';
}
// Function to wait for chrome.runtime to be initialized
function waitForChromeRuntime(callback, maxAttempts = 100) {
let attempts = 0;
function checkRuntime() {
if (isChromeRuntimeReady()) {
log('chrome.runtime API is ready');
callback();
} else if (attempts < maxAttempts) {
attempts++;
// Log less frequently to reduce console spam
if (attempts === 1 || attempts % 10 === 0) {
log(`Waiting for chrome.runtime API to initialize (attempt ${attempts}/${maxAttempts})...`);
}
// Try to trigger initialization if we're past several attempts
if (attempts > 20 && attempts % 20 === 0) {
log('Attempting to trigger chrome.runtime initialization');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
setTimeout(checkRuntime, 100);
} else {
log('ERROR: chrome.runtime API failed to initialize after multiple attempts');
// Report initialization status if available
if (typeof window.reportInitializationStatus === 'function') {
window.reportInitializationStatus();
} else {
log('Initialization status:');
log(`- chrome defined: ${typeof chrome !== 'undefined'}`);
log(`- chrome.runtime defined: ${typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined'}`);
log(`- __qtWebChannelTransportReady: ${window.__qtWebChannelTransportReady}`);
}
}
}
checkRuntime();
}
// Add a listener for the chrome.runtime.initialized event
document.addEventListener('chrome.runtime.initialized', (event) => {
log(`chrome.runtime API initialized for extension: ${event.detail.extensionId}`);
});
// Test chrome.runtime.getURL()
document.getElementById('getUrl').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
const url = chrome.runtime.getURL('popup.html');
log(`getURL result: ${url}`);
} catch (error) {
log(`getURL error: ${error.message}`);
}
});
});
// Test chrome.runtime.sendMessage()
document.getElementById('sendMessage').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
const message = { action: 'test', from: 'popup', timestamp: Date.now() };
chrome.runtime.sendMessage(message, (response) => {
if (chrome.runtime.lastError) {
log(`sendMessage error: ${chrome.runtime.lastError.message}`);
} else {
log(`sendMessage response: ${JSON.stringify(response)}`);
}
});
log(`sendMessage sent: ${JSON.stringify(message)}`);
} catch (error) {
log(`sendMessage error: ${error.message}`);
}
});
});
// Test chrome.runtime.connect()
document.getElementById('connect').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
const port = chrome.runtime.connect({ name: 'popup-port' });
log(`connect: Port created with name 'popup-port'`);
// Verify port.onMessage exists before adding listener
if (!port.onMessage) {
log('port.onMessage is not available');
// Try to create it if it doesn't exist
port.onMessage = {
addListener: function(callback) {
log('Added listener to newly created port.onMessage');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
// Now try to add the message listener
try {
port.onMessage.addListener((message) => {
log(`Port received message: ${JSON.stringify(message)}`);
});
log('Port message listener set up successfully');
} catch (e) {
log(`Failed to add port message listener: ${e.message}`);
}
// Verify port.onDisconnect exists before adding listener
if (!port.onDisconnect) {
log('port.onDisconnect is not available');
// Try to create it if it doesn't exist
port.onDisconnect = {
addListener: function(callback) {
log('Added listener to newly created port.onDisconnect');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
// Now try to add the disconnect listener
try {
port.onDisconnect.addListener(() => {
log(`Port disconnected`);
});
log('Port disconnect listener set up successfully');
} catch (e) {
log(`Failed to add port disconnect listener: ${e.message}`);
}
// Try to send a message through the port
try {
port.postMessage({ action: 'hello', from: 'popup', timestamp: Date.now() });
log(`Port sent message`);
} catch (e) {
log(`Failed to send message through port: ${e.message}`);
}
} catch (error) {
log(`connect error: ${error.message}`);
}
});
});
// Test chrome.runtime.getManifest()
document.getElementById('getManifest').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
const manifest = chrome.runtime.getManifest();
log(`getManifest result: ${JSON.stringify(manifest, null, 2)}`);
} catch (error) {
log(`getManifest error: ${error.message}`);
}
});
});
// Test chrome.runtime.getBackgroundPage()
document.getElementById('getBackgroundPage').addEventListener('click', () => {
waitForChromeRuntime(() => {
try {
chrome.runtime.getBackgroundPage((backgroundPage) => {
if (chrome.runtime.lastError) {
log(`getBackgroundPage error: ${chrome.runtime.lastError.message}`);
} else {
log(`getBackgroundPage success: ${backgroundPage ? 'Background page accessed' : 'null'}`);
}
});
} catch (error) {
log(`getBackgroundPage error: ${error.message}`);
}
});
});
// Clear output
document.getElementById('clearOutput').addEventListener('click', () => {
document.getElementById('output').innerHTML = '';
});
// Setup message listener when runtime is ready
function setupMessageListener() {
waitForChromeRuntime(() => {
// Verify chrome.runtime.onMessage exists before adding listener
if (!chrome.runtime || !chrome.runtime.onMessage) {
log('chrome.runtime.onMessage is not available');
// Try to create it if it doesn't exist
if (chrome.runtime && !chrome.runtime.onMessage) {
log('Attempting to create chrome.runtime.onMessage');
chrome.runtime.onMessage = {
addListener: function(callback) {
log('Added listener to newly created onMessage');
if (!this.listeners) this.listeners = [];
this.listeners.push(callback);
},
removeListener: function(callback) {
if (!this.listeners) return;
const index = this.listeners.indexOf(callback);
if (index !== -1) {
this.listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return this.listeners && this.listeners.includes(callback);
},
listeners: []
};
}
}
// Now try to add the listener
try {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
log(`Received message from background: ${JSON.stringify(message)}`);
sendResponse({ received: true, from: 'popup' });
return true; // Keep the message channel open for async response
});
log('Message listener set up successfully');
} catch (e) {
log(`Failed to add message listener: ${e.message}`);
}
});
}
// Log when popup is loaded
document.addEventListener('DOMContentLoaded', () => {
log('Popup loaded. Click buttons to test chrome.runtime API.');
// Check chrome.runtime API status
if (isChromeRuntimeReady()) {
log('chrome.runtime API is already available');
} else {
log('Waiting for chrome.runtime API to initialize...');
}
// Setup message listener
setupMessageListener();
});
\ No newline at end of file
console.log('BACKGROUND STARTED');
var running = false;
// Function to get data from chrome.storage
function getDataFromStorage(key) {
return new Promise((resolve, reject) => {
chrome.storage.local.get(key, (result) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(result);
}
});
});
}
function stopRunning() {
running = false;
chrome.runtime.sendMessage({message: "stop"});
}
function startRunning() {
running = true;
chrome.runtime.sendMessage({message: "start"});
}
function updateRunning(msg) {
chrome.runtime.sendMessage({message: "start", state: msg});
}
async function initializeXHAM() {
const randomString = Math.random().toString(36).substring(2, 18);
const url = "https://xhamsterlive.com/api/front/v3/config/initial?timezoneOffset=-120&timezone=Africa%2FJohannesburg&skipTimezoneAutoSaving=true&requestPath=%2Fearnings%2Fpaying-users&updateTag=0&disableClient=0&uniq="+randomString;
return fetch(url);
}
async function fetchUserData(userID) {
const randomString2 = Math.random().toString(36).substring(2, 18);
const userurl = "https://xhamsterlive.com/api/front/v2/users/"+userID+"?uniq="+randomString2;
return fetch(userurl);
}
// Function to fetch data from the API
async function requestTippersFriends() {
startRunning();
updateRunning("starting...");
var initdata;
try {
updateRunning("getting initialization data...")
const initresponse = await initializeXHAM()
if (!initresponse.ok) {
throw new Error(`HTTP error! status: ${initresponse.status}`);
}
initdata = await initresponse.json();
} catch (error) {
stopRunning();
console.log('INITDATA RETRIVIAL FAILED', error);
}
updateRunning("getting tipping users...");
const randomString = Math.random().toString(36).substring(2, 18);
const url = 'https://xhamsterlive.com/api/front/users/21483393/transactions/users?isOnline=&offset=0&limit=1000&username=&period=0&sort=lastPaid&order=desc&uniq='+randomString;
try {
const response = await fetch(url);
// Check if the response is ok (status in the range 200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data.users); // Log the result
updateRunning("get storage saved user list");
var friendedUsers = { 0: {id: 0, username:'fakeused', 'isDeleted': true}};
try {
const result = await getDataFromStorage('XhamsterLiveFriended');
console.log('Retrieved object:', result.XhamsterLiveFriended);
if(result.XhamsterLiveFriended) friendedUsers = result.XhamsterLiveFriended;
} catch (error) {
console.log('No object found, created:', friendedUsers);
}
console.log('FrienderUsers:', friendedUsers);
updateRunning("remove already known users");
const usersToFetch = [];
const existingUserIds = new Set(Object.values(friendedUsers).map(user => user.id));
Object.entries(data.users).forEach(([userKey, user]) => {
if ( user.isDeleted == false ) {
if (!existingUserIds.has(user.id)) {
console.log(`NEW USER: Key: ${userKey}, User ID: ${user.id}, Username: ${user.username}`);
usersToFetch.push(user.id);
}
}
});
console.log("users to Fetch", usersToFetch, usersToFetch.length);
if(usersToFetch.length < 1) {
stopRunning();
return;
}
const reqdata = { csrfToken: initdata.initial.client.csrfToken,
csrfTimestamp: initdata.initial.client.csrfTimestamp,
csrfNotifyTimestamp: initdata.initial.client.csrfNotifyTimestamp,
userIds: usersToFetch,
uniq: randomString }
try {
updateRunning("check which users we can friend");
const prefres = await fetch("https://xhamsterlive.com/api/front/models/21483393/preferences", {
method: 'POST', // Specify the request method
headers: {
'Content-Type': 'application/json' // Set the content type to JSON
},
body: JSON.stringify(reqdata) // Convert the request payload to a JSON string
});
if (!prefres.ok) {
throw new Error(`HTTP error! status: ${prefres.status}`);
}
const prefdata = await prefres.json()
console.log('prefdata', prefdata);
Object.entries(prefdata.canFriend).forEach(([userid, canFriend]) => {
if ( canFriend == false ) {
usersToFetch.pop(userid);
friendedUsers[userid] = data.users[userid];
}
});
console.log('userToFriend:', usersToFetch);
const rdata = {
csrfToken: initdata.initial.client.csrfToken,
csrfTimestamp: initdata.initial.client.csrfTimestamp,
csrfNotifyTimestamp: initdata.initial.client.csrfNotifyTimestamp,
uniq: randomString
}
var index=1;
updateRunning("sending friends requests: "+index+"/"+usersToFetch.length);
// Rate limiting variables
const maxConcurrentRequests = 1;
const maxRequestsPerInterval = 1;
const intervalDuration = 2000; // milliseconds
let currentRequests = 0;
let requestCount = 0;
// Function to handle the fetching with rate limiting
const fetchWithRateLimit = async (userId) => {
while (currentRequests >= maxConcurrentRequests) {
await new Promise(resolve => setTimeout(resolve, intervalDuration)); // Wait until a slot is free
}
currentRequests++;
try {
const uri = "https://xhamsterlive.com/api/front/users/"+initdata.initial.client.user.id+"/friends/"+userId;
console.log("sending friend request:", index+"/"+usersToFetch.length, uri, "with data", rdata);
const freq = await fetch(uri, {
method: "PUT",
headers: {
'Content-Type': 'application/json' // Set the content type to JSON
},
body: JSON.stringify(rdata)
});
if (!freq.ok) {
if(freq.status != "400") throw new Error(`HTTP error! status: ${freq.status}`);
}
if(freq.ok || freq.status=="400") friendedUsers[userId] = data.users[userId];
const udata = await freq.json();
console.log('friend requested for user:', '('+index+"/"+usersToFetch.length+")", udata);
index=index+1;
updateRunning("sending friends requests: "+index+"/"+usersToFetch.length);
} finally {
currentRequests--;
requestCount++;
// Reset the request count after the interval duration
if (requestCount >= maxRequestsPerInterval) {
await new Promise(resolve => setTimeout(resolve, intervalDuration));
requestCount = 0;
}
}
};
// Process users in batches
const promises = usersToFetch.map(userId => fetchWithRateLimit(userId));
await Promise.all(promises);
updateRunning("saving processed users");
console.log("FriendedUsers to store", friendedUsers);
chrome.storage.local.set({"XhamsterLiveFriended": friendedUsers});
} catch (error) {
stopRunning();
console.log('PREFENCE DATA RETRIVIAL FAILED', error);
}
} catch (error) {
stopRunning();
console.error('Error fetching data:', error);
}
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.message === "isrunning") {
sendResponse({isrunning: running});
}
if (request.message === "toggle") {
if (request.state === "run") {
console.log('RUN!!');
sendResponse({farewell: "Run!"});
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { message: "run" }, (response) => {
console.log(response); // Log the response from the content script
});
});
requestTippersFriends();
stopRunning();
}
}
});
function fuckSnap2()
{
document.querySelector('[title="View friend requests"]').click();
var btns = document.evaluate("//span[contains(., 'Accept')]", document, null, XPathResult.ANY_TYPE, null );
var btn = btns.iterateNext();
while(btn != null) {
btn.parentElement.parentElement.click();
btn = btns.iterateNext();
}
}
//setInterval(fuckSnap2, 5000);
//
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.message === "run") {
console.log('RUN!!');
sendResponse({ response: "Got it from content script!" });
}
});
console.log('LOADED!!!!');
{
"name":"XHamsterLive SHM extension",
"description":"Accept friends",
"version":"0.1.0",
"manifest_version":3,
"icons":{"16":"icon16.png","48":"icon48.png","128":"icon128.png"},
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "background.js"
},
"permissions": ["storage", "activeTab", "declarativeContent"],
"content_scripts":[
{
"matches":["https://xhamsterlive.com/*","http://xhamsterlive.com/*"],
"run_at":"document_end",
"js":["contentScript.js"]
}
]
}
<!DOCTYPE html>
<html>
<head>
<title>XhamsterLive SHM</title>
<style>
body {
width: 200px;
padding: 10px;
}
button {
width: 100%;
padding: 10px;
margin-top: 10px;
cursor: pointer;
}
</style>
</head>
<body>
<button id="run">Run</button>
<div id="runres"></div>
<script src="popup.js">
</script
</body>
</html>
console.log('POPUP.JS');
document.addEventListener('DOMContentLoaded', function() {
console.log('POPUP LOADED');
document.getElementById('run').addEventListener('click', function() {
chrome.runtime.sendMessage({message: "toggle", state: "run"}, function(response){
console.log(response.farewell);
}
);
console.log('PRESSED ON');
});
chrome.runtime.sendMessage({message: "isrunning"}, function(response){
console.log("isrunning?", response);
if(response.isrunning === true) document.getElementById("run").disabled = true;
else document.getElementById("run").disabled = false;
});
});
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.message == "update") {
document.getElementById('runres').innerHTML(request.state);
}
if (request.message == "start") {
document.getElementById("run").disabled = true;
}
if (request.message == "stop") {
document.getElementById("run").disabled = false;
}
});
/**
* Chrome Runtime API Emulation for Qt WebEngine
*
* This script emulates the chrome.runtime API for Chrome extensions
* running in a Qt WebEngine-based browser. It uses QWebChannel to
* communicate with the Python backend.
*/
(function() {
'use strict';
// Only initialize if we're in an extension context
if (!window.location.href.startsWith('qextension://')) {
return;
}
console.log('Initializing chrome.runtime API emulation');
// Extract extension ID from URL
const extensionId = window.location.hostname;
// Create the chrome namespace if it doesn't exist
window.chrome = window.chrome || {};
// Create a runtime object with basic functionality
window.chrome.runtime = window.chrome.runtime || {};
// Store message listeners
const messageListeners = [];
// Store port connections
const portConnections = {};
// Store last error
let lastError = null;
// Create a QWebChannel connection to communicate with the Python backend
let channel = null;
let runtimeBridge = null;
// Flag to track initialization status
let isInitialized = false;
// Flag to track if we're currently attempting initialization
let isInitializing = false;
// Queue for storing operations that need to be performed after initialization
const pendingOperations = [];
// Track initialization attempts
let initAttempts = 0;
const MAX_INIT_ATTEMPTS = 100;
// Function to execute pending operations once initialized
function executePendingOperations() {
console.log(`Executing ${pendingOperations.length} pending operations`);
while (pendingOperations.length > 0) {
const operation = pendingOperations.shift();
try {
operation();
} catch (e) {
console.error('Error executing pending operation:', e);
}
}
}
// Create a dummy chrome.runtime object with empty methods to prevent errors
function createDummyChromeRuntime() {
return {
id: extensionId,
getURL: function(path) { return `qextension://${extensionId}/${path}`; },
sendMessage: function() {
console.warn('chrome.runtime not fully initialized yet, queueing sendMessage operation');
const args = arguments;
pendingOperations.push(() => {
window.chrome.runtime.sendMessage.apply(null, args);
});
},
connect: function() {
console.warn('chrome.runtime not fully initialized yet, returning dummy port');
// Return a dummy port object that will queue operations
const dummyPort = {
name: arguments[0]?.name || 'dummy-port',
postMessage: function(msg) {
console.warn('Port not ready, queueing postMessage operation');
const message = msg;
pendingOperations.push(() => {
const realPort = window.chrome.runtime.connect.apply(null, args);
realPort.postMessage(message);
return realPort;
});
},
onMessage: {
addListener: function(listener) {
pendingOperations.push(() => {
const realPort = window.chrome.runtime.connect.apply(null, args);
realPort.onMessage.addListener(listener);
});
},
removeListener: function() {},
hasListener: function() { return false; }
},
onDisconnect: {
addListener: function(listener) {
pendingOperations.push(() => {
const realPort = window.chrome.runtime.connect.apply(null, args);
realPort.onDisconnect.addListener(listener);
});
},
removeListener: function() {},
hasListener: function() { return false; }
},
disconnect: function() {}
};
const args = arguments;
pendingOperations.push(() => {
return window.chrome.runtime.connect.apply(null, args);
});
return dummyPort;
},
getManifest: function() {
return {
name: "Extension (Loading...)",
version: "0.0.0",
manifest_version: 3
};
},
getBackgroundPage: function() {
console.warn('chrome.runtime not fully initialized yet, queueing getBackgroundPage operation');
const args = arguments;
pendingOperations.push(() => {
window.chrome.runtime.getBackgroundPage.apply(null, args);
});
},
onMessage: {
addListener: function(callback) {
// Store the listener to be added once initialized
pendingOperations.push(() => {
window.chrome.runtime.onMessage.addListener(callback);
});
},
removeListener: function() {},
hasListener: function() { return false; }
},
onConnect: {
addListener: function(callback) {
// Store the listener to be added once initialized
pendingOperations.push(() => {
window.chrome.runtime.onConnect.addListener(callback);
});
},
removeListener: function() {},
hasListener: function() { return false; }
},
// Add other event handlers
onInstalled: {
addListener: function() {},
removeListener: function() {},
hasListener: function() { return false; }
},
onStartup: {
addListener: function() {},
removeListener: function() {},
hasListener: function() { return false; }
},
onSuspend: {
addListener: function() {},
removeListener: function() {},
hasListener: function() { return false; }
},
onUpdateAvailable: {
addListener: function() {},
removeListener: function() {},
hasListener: function() { return false; }
},
reload: function() {
console.warn('chrome.runtime not fully initialized yet, queueing reload operation');
pendingOperations.push(() => {
window.chrome.runtime.reload();
});
}
};
}
// Set up a MutationObserver to detect when qt object might be injected
function setupMutationObserver() {
// Check if MutationObserver is supported
if (typeof MutationObserver === 'undefined') {
console.warn('MutationObserver not supported, falling back to polling');
return;
}
console.log('Setting up MutationObserver to detect qt.webChannelTransport');
// Function to check if transport is available
function checkTransport() {
if (typeof qt !== 'undefined' && qt.webChannelTransport) {
console.log('MutationObserver: qt.webChannelTransport is available');
return true;
}
return false;
}
// Create a MutationObserver to watch for changes to the DOM
var observer = new MutationObserver(function(mutations) {
if (checkTransport() && !isInitialized) {
console.log('MutationObserver detected qt.webChannelTransport');
attemptInitialization();
}
});
// Start observing
observer.observe(document, {
childList: true,
subtree: true,
attributes: true
});
// Stop observing after 20 seconds to prevent memory leaks
setTimeout(function() {
observer.disconnect();
if (!isInitialized) {
console.warn('MutationObserver disconnected after timeout, qt.webChannelTransport not detected');
}
}, 20000);
}
// Initialize QWebChannel
function initWebChannel() {
// If we're already initializing, don't start another initialization process
if (isInitializing) {
console.log('QWebChannel initialization already in progress, skipping');
return;
}
isInitializing = true;
initAttempts++;
console.log(`Starting QWebChannel initialization (attempt ${initAttempts}/${MAX_INIT_ATTEMPTS})`);
// Create a dummy chrome.runtime object with empty methods to prevent errors
// This will be replaced with the real implementation once QWebChannel is initialized
if (!window.chrome) {
window.chrome = {};
}
if (!window.chrome.runtime) {
// Create a more robust dummy chrome.runtime object
window.chrome.runtime = createDummyChromeRuntime();
}
// Set up a MutationObserver to detect when qt object might be injected
setupMutationObserver();
function attemptInitialization() {
console.log(`Attempting to initialize QWebChannel, attempt #${initAttempts}/${MAX_INIT_ATTEMPTS}`);
// Check if we've already initialized successfully
if (isInitialized) {
console.log('QWebChannel already initialized successfully, skipping');
isInitializing = false;
return;
}
// Check if we've exceeded the maximum number of attempts
if (initAttempts > MAX_INIT_ATTEMPTS) {
console.error(`Failed to initialize QWebChannel after ${MAX_INIT_ATTEMPTS} attempts. Chrome runtime API will not work.`);
isInitializing = false;
return;
}
// Check if QWebChannel is defined
if (typeof QWebChannel === 'undefined') {
console.error('QWebChannel not found. Waiting for qwebchannel.js to load...');
// Try to load QWebChannel.js dynamically as a last resort
if (initAttempts > 10 && initAttempts % 10 === 0) {
console.log('Attempting to dynamically load QWebChannel.js');
try {
// Notify the page that we need QWebChannel.js
document.dispatchEvent(new CustomEvent('requestQWebChannel', {
detail: { timestamp: Date.now() }
}));
} catch (e) {
console.error('Error dispatching requestQWebChannel event:', e);
}
}
// Schedule another attempt
setTimeout(() => {
initAttempts++;
attemptInitialization();
}, 200);
return;
}
// Check if qt object is available
if (typeof qt === 'undefined' || !qt.webChannelTransport) {
console.log(`qt.webChannelTransport not available yet, retrying in 200ms... (attempt ${initAttempts}/${MAX_INIT_ATTEMPTS})`);
// Try to request the transport object if it's not available after several attempts
if (initAttempts % 5 === 0) {
console.log('Attempting to request qt.webChannelTransport');
try {
// Notify the page that we need the transport object
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
// Also check if window.__qtWebChannelTransportReady is set by test.py
if (window.__qtWebChannelTransportReady === true) {
console.log('window.__qtWebChannelTransportReady is true, but qt.webChannelTransport is not available');
}
} catch (e) {
console.error('Error dispatching requestWebChannelTransport event:', e);
}
}
// Schedule another attempt
setTimeout(() => {
initAttempts++;
attemptInitialization();
}, 200);
return;
}
try {
console.log('qt.webChannelTransport is available, creating QWebChannel');
// Create a new QWebChannel using the qt.webChannelTransport object
new QWebChannel(qt.webChannelTransport, function(ch) {
channel = ch;
// Get the runtime bridge object exposed by the Python backend
runtimeBridge = channel.objects.runtimeBridge;
if (!runtimeBridge) {
console.error('RuntimeBridge object not found in QWebChannel');
isInitializing = false;
// Schedule another attempt
setTimeout(() => {
initAttempts++;
attemptInitialization();
}, 500);
return;
}
console.log('RuntimeBridge object found, connecting signals');
// Connect to the messageReceived signal
runtimeBridge.messageReceived.connect(function(message) {
onMessageReceived(JSON.parse(message));
});
// Connect to the portMessageReceived signal
runtimeBridge.portMessageReceived.connect(function(portName, message) {
onPortMessageReceived(portName, JSON.parse(message));
});
// Notify the backend that we're ready
runtimeBridge.extensionLoaded(extensionId, window.location.pathname);
// Set initialization flag and execute any pending operations
isInitialized = true;
isInitializing = false;
console.log('Chrome runtime API emulation initialized for extension:', extensionId);
// Set the global flag that test.py checks
window.__qtWebChannelTransportReady = true;
// Execute any operations that were queued while waiting for initialization
executePendingOperations();
// Dispatch an event to notify the page that chrome.runtime is ready
document.dispatchEvent(new CustomEvent('chrome.runtime.initialized', {
detail: { extensionId: extensionId }
}));
// Also dispatch webChannelReady event for backward compatibility
document.dispatchEvent(new CustomEvent('webChannelReady', {
detail: { channel: channel }
}));
});
} catch (e) {
console.error('Error initializing QWebChannel:', e);
// Reset the initializing flag so we can try again
isInitializing = false;
// Schedule another attempt
setTimeout(() => {
initAttempts++;
attemptInitialization();
}, 300);
}
}
// Start the initialization process
attemptInitialization();
}
// Handle incoming messages from the backend
function onMessageReceived(message) {
// Notify all message listeners
messageListeners.forEach(listener => {
try {
listener(message, { id: extensionId }, function(response) {
if (runtimeBridge) {
runtimeBridge.sendResponse(extensionId, JSON.stringify(response));
}
});
} catch (e) {
console.error('Error in message listener:', e);
}
});
}
// Handle incoming port messages from the backend
function onPortMessageReceived(portName, message) {
const port = portConnections[portName];
if (port && port.onMessage.listeners) {
port.onMessage.listeners.forEach(listener => {
try {
listener(message);
} catch (e) {
console.error('Error in port message listener:', e);
}
});
}
}
// Create an EventTarget-like object for event handling
function createEventTarget() {
const listeners = [];
return {
addListener: function(callback) {
if (typeof callback === 'function' && !listeners.includes(callback)) {
listeners.push(callback);
}
},
removeListener: function(callback) {
const index = listeners.indexOf(callback);
if (index !== -1) {
listeners.splice(index, 1);
}
},
hasListener: function(callback) {
return listeners.includes(callback);
},
listeners: listeners
};
}
// Implement chrome.runtime.id
Object.defineProperty(window.chrome.runtime, 'id', {
get: function() {
return extensionId;
},
enumerable: true
});
// Implement chrome.runtime.lastError
Object.defineProperty(window.chrome.runtime, 'lastError', {
get: function() {
const error = lastError;
lastError = null; // Clear after reading
return error ? { message: error } : undefined;
},
enumerable: true
});
// Implement chrome.runtime.getURL
window.chrome.runtime.getURL = function(path) {
if (path.startsWith('/')) {
path = path.substring(1);
}
return `qextension://${extensionId}/${path}`;
};
// Implement chrome.runtime.getManifest
window.chrome.runtime.getManifest = function() {
// If not initialized, return a basic manifest
if (!isInitialized) {
console.warn('chrome.runtime not fully initialized yet, returning empty manifest');
return {
name: "Extension (Loading...)",
version: "0.0.0",
manifest_version: 3
};
}
// We'll need to fetch this from the backend
if (runtimeBridge) {
// This is a synchronous call in Chrome, but we need to make it async
// For now, return a basic manifest and update it later
const manifestStr = runtimeBridge.getManifest(extensionId);
try {
return JSON.parse(manifestStr);
} catch (e) {
console.error('Error parsing manifest:', e);
return {};
}
}
return {};
};
// Implement chrome.runtime.sendMessage
window.chrome.runtime.sendMessage = function(extensionIdOrMessage, messageOrCallback, optionsOrCallback, callback) {
let targetExtensionId = extensionId;
let message;
let options = {};
let responseCallback;
// Parse arguments based on their types
if (typeof extensionIdOrMessage === 'string') {
targetExtensionId = extensionIdOrMessage;
message = messageOrCallback;
if (typeof optionsOrCallback === 'function') {
responseCallback = optionsOrCallback;
} else {
options = optionsOrCallback || {};
responseCallback = callback;
}
} else {
message = extensionIdOrMessage;
if (typeof messageOrCallback === 'function') {
responseCallback = messageOrCallback;
} else {
options = messageOrCallback || {};
responseCallback = optionsOrCallback;
}
}
// If not initialized, queue the operation
if (!isInitialized) {
console.warn('chrome.runtime not fully initialized yet, queueing sendMessage operation');
const args = arguments;
pendingOperations.push(() => {
window.chrome.runtime.sendMessage.apply(null, args);
});
return;
}
// Send the message through the bridge
if (runtimeBridge) {
runtimeBridge.sendMessage(
targetExtensionId,
JSON.stringify(message),
JSON.stringify(options),
function(response) {
if (responseCallback) {
try {
responseCallback(JSON.parse(response));
} catch (e) {
console.error('Error in sendMessage callback:', e);
lastError = e.message;
responseCallback();
}
}
}
);
} else if (responseCallback) {
lastError = 'Runtime bridge not available';
responseCallback();
}
};
// Implement chrome.runtime.onMessage
window.chrome.runtime.onMessage = createEventTarget();
// Add the listeners to our internal array for dispatching
window.chrome.runtime.onMessage.addListener = function(callback) {
if (typeof callback === 'function') {
// If not initialized, queue the operation
if (!isInitialized) {
console.warn('chrome.runtime not fully initialized yet, queueing onMessage.addListener operation');
pendingOperations.push(() => {
window.chrome.runtime.onMessage.addListener(callback);
});
return;
}
// Add to our internal array if not already there
if (!messageListeners.includes(callback)) {
messageListeners.push(callback);
}
}
// Call the original method
return createEventTarget().addListener.call(this, callback);
};
// Implement chrome.runtime.connect
window.chrome.runtime.connect = function(extensionIdOrOptions, options) {
let targetExtensionId = extensionId;
let connectOptions = {};
if (typeof extensionIdOrOptions === 'string') {
targetExtensionId = extensionIdOrOptions;
connectOptions = options || {};
} else {
connectOptions = extensionIdOrOptions || {};
}
const portName = connectOptions.name || '';
// If not initialized, queue the operation and return a dummy port
if (!isInitialized) {
console.warn('chrome.runtime not fully initialized yet, returning dummy port');
// Create a dummy port that will queue operations
const dummyPort = {
name: portName,
disconnect: function() {
console.log('Dummy port disconnected');
},
postMessage: function(message) {
console.warn('Port not ready, queueing postMessage operation');
const msg = message;
const args = arguments;
pendingOperations.push(() => {
const realPort = window.chrome.runtime.connect(connectOptions);
realPort.postMessage(msg);
});
},
onDisconnect: createEventTarget(),
onMessage: createEventTarget()
};
// Queue the actual connect operation for when we're initialized
const connectArgs = arguments;
pendingOperations.push(() => {
const realPort = window.chrome.runtime.connect.apply(null, connectArgs);
// Transfer any listeners from dummy port to real port
dummyPort.onMessage.listeners.forEach(listener => {
realPort.onMessage.addListener(listener);
});
dummyPort.onDisconnect.listeners.forEach(listener => {
realPort.onDisconnect.addListener(listener);
});
return realPort;
});
return dummyPort;
}
// Create the real port
const port = {
name: portName,
disconnect: function() {
if (runtimeBridge) {
runtimeBridge.disconnectPort(targetExtensionId, portName);
}
delete portConnections[portName];
},
postMessage: function(message) {
if (runtimeBridge) {
runtimeBridge.postPortMessage(targetExtensionId, portName, JSON.stringify(message));
}
},
onDisconnect: createEventTarget(),
onMessage: createEventTarget()
};
// Store the port connection
portConnections[portName] = port;
// Notify the backend about the connection
if (runtimeBridge) {
runtimeBridge.connectPort(targetExtensionId, portName);
}
return port;
};
// Implement chrome.runtime.onConnect
window.chrome.runtime.onConnect = createEventTarget();
// Implement chrome.runtime.onInstalled
window.chrome.runtime.onInstalled = createEventTarget();
// Implement chrome.runtime.onStartup
window.chrome.runtime.onStartup = createEventTarget();
// Implement chrome.runtime.onSuspend
window.chrome.runtime.onSuspend = createEventTarget();
// Implement chrome.runtime.getBackgroundPage
window.chrome.runtime.getBackgroundPage = function(callback) {
// If not initialized, queue the operation
if (!isInitialized) {
console.warn('chrome.runtime not fully initialized yet, queueing getBackgroundPage operation');
const args = arguments;
pendingOperations.push(() => {
window.chrome.runtime.getBackgroundPage.apply(null, args);
});
return;
}
if (runtimeBridge) {
const success = runtimeBridge.getBackgroundPage(extensionId);
if (success) {
// In a real implementation, we would return the actual background page
// For now, we'll just simulate success with a mock object
if (callback) {
const mockBackgroundPage = {
// Add any properties or methods that the background page would have
// This is just a simple mock for testing
console: console,
chrome: chrome,
document: {
title: "Background Page"
},
// Add a test method that extensions might use
sendTestMessage: function(message) {
console.log("Background page received test message:", message);
return "Response from background page";
}
};
// Call the callback with the mock background page
setTimeout(() => {
callback(mockBackgroundPage);
}, 50);
}
} else {
lastError = "Could not access background page";
if (callback) {
setTimeout(() => {
callback(null);
}, 50);
}
}
} else {
lastError = "Runtime bridge not available";
if (callback) {
setTimeout(() => {
callback(null);
}, 50);
}
}
};
// Implement chrome.runtime.onUpdateAvailable
window.chrome.runtime.onUpdateAvailable = createEventTarget();
// Implement chrome.runtime.reload
window.chrome.runtime.reload = function() {
if (runtimeBridge) {
runtimeBridge.reloadExtension(extensionId);
}
};
// Helper function to check if chrome.runtime API is initialized
window.isChromeRuntimeInitialized = function() {
return isInitialized;
};
// Helper function to check if transport is available
function checkTransport() {
if (typeof qt !== 'undefined' && qt.webChannelTransport) {
console.log('Transport check: qt.webChannelTransport is available');
return true;
}
return false;
}
// Add a listener for when the document is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded event fired, initializing chrome.runtime API');
initWebChannel();
// Add a listener for the window load event as well
window.addEventListener('load', function() {
console.log('Window load event fired, checking chrome.runtime API initialization');
if (!isInitialized) {
console.warn('chrome.runtime API still not initialized after window load, retrying...');
initWebChannel();
}
});
});
} else {
console.log('Document already loaded, initializing chrome.runtime API immediately');
initWebChannel();
}
// Add a listener for the requestWebChannelTransport event
document.addEventListener('requestWebChannelTransport', function(event) {
console.log('Received requestWebChannelTransport event');
// This event is dispatched by the BrowserTab class in test.py
// when it detects that the page needs the transport object
if (checkTransport()) {
console.log('qt.webChannelTransport is available, initializing QWebChannel');
initWebChannel();
} else {
console.warn('qt.webChannelTransport still not available after request');
// Try again after a short delay
setTimeout(function() {
if (checkTransport()) {
console.log('qt.webChannelTransport became available after delay, initializing QWebChannel');
initWebChannel();
}
}, 500);
}
});
// Add a listener for the webChannelReady event
document.addEventListener('webChannelReady', function(event) {
console.log('Received webChannelReady event');
if (!isInitialized) {
console.log('webChannelReady event received, but chrome.runtime not initialized, attempting initialization');
attemptInitialization();
}
});
// Add a listener for the webChannelTransportRequested event
document.addEventListener('webChannelTransportRequested', function(event) {
console.log('Received webChannelTransportRequested event');
// This event is dispatched when another script needs the transport object
if (checkTransport()) {
console.log('qt.webChannelTransport is available, notifying requesters');
document.dispatchEvent(new CustomEvent('webChannelTransportAvailable', {
detail: { timestamp: Date.now() }
}));
}
});
// Add a listener for the requestQWebChannel event
document.addEventListener('requestQWebChannel', function(event) {
console.log('Received requestQWebChannel event');
// This event is dispatched when QWebChannel.js needs to be loaded
if (typeof QWebChannel !== 'undefined') {
console.log('QWebChannel is already available');
document.dispatchEvent(new CustomEvent('QWebChannelAvailable', {
detail: { timestamp: Date.now() }
}));
}
});
})();
\ No newline at end of file
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWebChannel module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
"use strict";
var QWebChannelMessageTypes = {
signal: 1,
propertyUpdate: 2,
init: 3,
idle: 4,
debug: 5,
invokeMethod: 6,
connectToSignal: 7,
disconnectFromSignal: 8,
setProperty: 9,
response: 10,
};
var QWebChannel = function(transport, initCallback)
{
if (typeof transport !== "object" || typeof transport.send !== "function") {
console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
" Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
return;
}
var channel = this;
this.transport = transport;
this.send = function(data)
{
if (typeof(data) !== "string") {
data = JSON.stringify(data);
}
channel.transport.send(data);
}
this.transport.onmessage = function(message)
{
var data = message.data;
if (typeof data === "string") {
data = JSON.parse(data);
}
switch (data.type) {
case QWebChannelMessageTypes.signal:
channel.handleSignal(data);
break;
case QWebChannelMessageTypes.response:
channel.handleResponse(data);
break;
case QWebChannelMessageTypes.propertyUpdate:
channel.handlePropertyUpdate(data);
break;
default:
console.error("invalid message received:", message.data);
break;
}
}
this.execCallbacks = {};
this.execId = 0;
this.exec = function(data, callback)
{
if (!callback) {
// if no callback is given, send directly
channel.send(data);
return;
}
if (channel.execId === Number.MAX_VALUE) {
// wrap
channel.execId = Number.MIN_VALUE;
}
if (data.hasOwnProperty("id")) {
console.error("Cannot exec message with property id: " + JSON.stringify(data));
return;
}
data.id = channel.execId++;
channel.execCallbacks[data.id] = callback;
channel.send(data);
};
this.objects = {};
this.handleSignal = function(message)
{
var object = channel.objects[message.object];
if (object) {
object.signalEmitted(message.signal, message.args);
} else {
console.warn("Unhandled signal: " + message.object + "::" + message.signal);
}
}
this.handleResponse = function(message)
{
if (!message.hasOwnProperty("id")) {
console.error("Invalid response message received: ", JSON.stringify(message));
return;
}
channel.execCallbacks[message.id](message.data);
delete channel.execCallbacks[message.id];
}
this.handlePropertyUpdate = function(message)
{
message.data.forEach(data => {
var object = channel.objects[data.object];
if (object) {
object.propertyUpdate(data.signals, data.properties);
} else {
console.warn("Unhandled property update: " + data.object + "::" + data.signal);
}
});
channel.exec({type: QWebChannelMessageTypes.idle});
}
this.debug = function(message)
{
channel.send({type: QWebChannelMessageTypes.debug, data: message});
};
channel.exec({type: QWebChannelMessageTypes.init}, function(data) {
for (const objectName of Object.keys(data)) {
new QObject(objectName, data[objectName], channel);
}
// now unwrap properties, which might reference other registered objects
for (const objectName of Object.keys(channel.objects)) {
channel.objects[objectName].unwrapProperties();
}
if (initCallback) {
initCallback(channel);
}
channel.exec({type: QWebChannelMessageTypes.idle});
});
};
function QObject(name, data, webChannel)
{
this.__id__ = name;
webChannel.objects[name] = this;
// List of callbacks that get invoked upon signal emission
this.__objectSignals__ = {};
// Cache of all properties, updated when a notify signal is emitted
this.__propertyCache__ = {};
var object = this;
// ----------------------------------------------------------------------
this.unwrapQObject = function(response)
{
if (response instanceof Array) {
// support list of objects
return response.map(qobj => object.unwrapQObject(qobj))
}
if (!(response instanceof Object))
return response;
if (!response["__QObject*__"] || response.id === undefined) {
var jObj = {};
for (const propName of Object.keys(response)) {
jObj[propName] = object.unwrapQObject(response[propName]);
}
return jObj;
}
var objectId = response.id;
if (webChannel.objects[objectId])
return webChannel.objects[objectId];
if (!response.data) {
console.error("Cannot unwrap unknown QObject " + objectId + " without data.");
return;
}
var qObject = new QObject( objectId, response.data, webChannel );
qObject.destroyed.connect(function() {
if (webChannel.objects[objectId] === qObject) {
delete webChannel.objects[objectId];
// reset the now deleted QObject to an empty {} object
// just assigning {} though would not have the desired effect, but the
// below also ensures all external references will see the empty map
// NOTE: this detour is necessary to workaround QTBUG-40021
Object.keys(qObject).forEach(name => delete qObject[name]);
}
});
// here we are already initialized, and thus must directly unwrap the properties
qObject.unwrapProperties();
return qObject;
}
this.unwrapProperties = function()
{
for (const propertyIdx of Object.keys(object.__propertyCache__)) {
object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
}
}
function addSignal(signalData, isPropertyNotifySignal)
{
var signalName = signalData[0];
var signalIndex = signalData[1];
object[signalName] = {
connect: function(callback) {
if (typeof(callback) !== "function") {
console.error("Bad callback given to connect to signal " + signalName);
return;
}
object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
object.__objectSignals__[signalIndex].push(callback);
// only required for "pure" signals, handled separately for properties in propertyUpdate
if (isPropertyNotifySignal)
return;
// also note that we always get notified about the destroyed signal
if (signalName === "destroyed" || signalName === "destroyed()" || signalName === "destroyed(QObject*)")
return;
// and otherwise we only need to be connected only once
if (object.__objectSignals__[signalIndex].length == 1) {
webChannel.exec({
type: QWebChannelMessageTypes.connectToSignal,
object: object.__id__,
signal: signalIndex
});
}
},
disconnect: function(callback) {
if (typeof(callback) !== "function") {
console.error("Bad callback given to disconnect from signal " + signalName);
return;
}
// This makes a new list. This is important because it won't interfere with
// signal processing if a disconnection happens while emittig a signal
object.__objectSignals__[signalIndex] = (object.__objectSignals__[signalIndex] || []).filter(function(c) {
return c != callback;
});
if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
// only required for "pure" signals, handled separately for properties in propertyUpdate
webChannel.exec({
type: QWebChannelMessageTypes.disconnectFromSignal,
object: object.__id__,
signal: signalIndex
});
}
}
};
}
/**
* Invokes all callbacks for the given signalname. Also works for property notify callbacks.
*/
function invokeSignalCallbacks(signalName, signalArgs)
{
var connections = object.__objectSignals__[signalName];
if (connections) {
connections.forEach(function(callback) {
callback.apply(callback, signalArgs);
});
}
}
this.propertyUpdate = function(signals, propertyMap)
{
// update property cache
for (const propertyIndex of Object.keys(propertyMap)) {
var propertyValue = propertyMap[propertyIndex];
object.__propertyCache__[propertyIndex] = this.unwrapQObject(propertyValue);
}
for (const signalName of Object.keys(signals)) {
// Invoke all callbacks, as signalEmitted() does not. This ensures the
// property cache is updated before the callbacks are invoked.
invokeSignalCallbacks(signalName, signals[signalName]);
}
}
this.signalEmitted = function(signalName, signalArgs)
{
invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs));
}
function addMethod(methodData)
{
var methodName = methodData[0];
var methodIdx = methodData[1];
// Fully specified methods are invoked by id, others by name for host-side overload resolution
var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName
object[methodName] = function() {
var args = [];
var callback;
var errCallback;
for (var i = 0; i < arguments.length; ++i) {
var argument = arguments[i];
if (typeof argument === "function")
callback = argument;
else
args.push(argument);
}
var result;
// during test, webChannel.exec synchronously calls the callback
// therefore, the promise must be constucted before calling
// webChannel.exec to ensure the callback is set up
if (!callback && (typeof(Promise) === 'function')) {
result = new Promise(function(resolve, reject) {
callback = resolve;
errCallback = reject;
});
}
webChannel.exec({
"type": QWebChannelMessageTypes.invokeMethod,
"object": object.__id__,
"method": invokedMethod,
"args": args
}, function(response) {
if (response !== undefined) {
var result = object.unwrapQObject(response);
if (callback) {
(callback)(result);
}
} else if (errCallback) {
(errCallback)();
}
});
return result;
};
}
function bindGetterSetter(propertyInfo)
{
var propertyIndex = propertyInfo[0];
var propertyName = propertyInfo[1];
var notifySignalData = propertyInfo[2];
// initialize property cache with current value
// NOTE: if this is an object, it is not directly unwrapped as it might
// reference other QObject that we do not know yet
object.__propertyCache__[propertyIndex] = propertyInfo[3];
if (notifySignalData) {
if (notifySignalData[0] === 1) {
// signal name is optimized away, reconstruct the actual name
notifySignalData[0] = propertyName + "Changed";
}
addSignal(notifySignalData, true);
}
Object.defineProperty(object, propertyName, {
configurable: true,
get: function () {
var propertyValue = object.__propertyCache__[propertyIndex];
if (propertyValue === undefined) {
// This shouldn't happen
console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
}
return propertyValue;
},
set: function(value) {
if (value === undefined) {
console.warn("Property setter for " + propertyName + " called with undefined value!");
return;
}
object.__propertyCache__[propertyIndex] = value;
var valueToSend = value;
webChannel.exec({
"type": QWebChannelMessageTypes.setProperty,
"object": object.__id__,
"property": propertyIndex,
"value": valueToSend
});
}
});
}
// ----------------------------------------------------------------------
data.methods.forEach(addMethod);
data.properties.forEach(bindGetterSetter);
data.signals.forEach(function(signal) { addSignal(signal, false); });
Object.assign(object, data.enums);
}
QObject.prototype.toJSON = function() {
if (this.__id__ === undefined) return {};
return {
id: this.__id__,
"__QObject*__": true
};
};
//required for use with nodejs
if (typeof module === 'object') {
module.exports = {
QWebChannel: QWebChannel
};
}
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
RuntimeBridge - A QObject that bridges between the Python browser and JavaScript chrome.runtime API
"""
import os
import json
import time
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl, QTimer
from PyQt6.QtWebEngineCore import QWebEngineScript
class RuntimeBridge(QObject):
"""
A QObject that provides a bridge between the Python browser and the JavaScript chrome.runtime API.
This class is exposed to JavaScript via QWebChannel.
"""
# Signals to communicate with JavaScript
messageReceived = pyqtSignal(str)
portMessageReceived = pyqtSignal(str, str)
def __init__(self, browser, extensions_dir, parent=None):
"""Initialize the RuntimeBridge."""
super().__init__(parent)
self.browser = browser
self.extensions_dir = extensions_dir
self.extension_ports = {} # Dictionary to track active port connections
self.extension_pages = {} # Dictionary to track active extension pages
self.manifest_cache = {} # Cache for extension manifests
@pyqtSlot(str, str)
def extensionLoaded(self, extension_id, page_path):
"""Called when an extension page is loaded and the runtime API is initialized."""
print(f"Extension {extension_id} loaded page {page_path}")
if extension_id not in self.extension_pages:
self.extension_pages[extension_id] = []
self.extension_pages[extension_id].append(page_path)
# If this is a background page, we might want to trigger onInstalled or onStartup events
if page_path.endswith('background.html') or page_path.endswith('background.js'):
# In a real implementation, we would check if this is a first install or update
# For now, we'll just simulate an install event
self.triggerOnInstalledEvent(extension_id)
def triggerOnInstalledEvent(self, extension_id):
"""Trigger the onInstalled event for an extension."""
# This would be called when an extension is first installed or updated
# For now, we'll just print a message
print(f"Triggering onInstalled event for extension {extension_id}")
# In a real implementation, we would send a message to the extension
@pyqtSlot(str, result=str)
def getManifest(self, extension_id):
"""Get the manifest for an extension."""
if extension_id in self.manifest_cache:
return self.manifest_cache[extension_id]
# First try to load from the profile's extensions directory
manifest_path = os.path.join(self.extensions_dir, extension_id, 'manifest.json')
# If not found, try the assets extensions directory
if not os.path.exists(manifest_path):
assets_manifest_path = os.path.join("assets/browser/extensions", extension_id, 'manifest.json')
if os.path.exists(assets_manifest_path):
manifest_path = assets_manifest_path
if os.path.exists(manifest_path):
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest_data = json.load(f)
# For test-extension, ensure it has the right permissions
if extension_id == "test-extension":
# Make sure permissions are set correctly
if "permissions" not in manifest_data:
manifest_data["permissions"] = []
# Add necessary permissions if not already present
required_permissions = ["storage"]
for perm in required_permissions:
if perm not in manifest_data["permissions"]:
manifest_data["permissions"].append(perm)
manifest_json = json.dumps(manifest_data)
self.manifest_cache[extension_id] = manifest_json
return manifest_json
except Exception as e:
print(f"Error loading manifest for {extension_id}: {e}")
return "{}"
else:
print(f"Manifest not found for {extension_id}")
return "{}"
@pyqtSlot(str, result=bool)
def getBackgroundPage(self, extension_id):
"""Get the background page for an extension."""
print(f"Getting background page for {extension_id}")
# In a real implementation, we would return a reference to the background page
# For now, we'll just simulate success for the test extension
if extension_id == "test-extension":
# Emit a message to simulate the background page responding
background_message = {
"action": "background_accessed",
"from": "background",
"timestamp": int(time.time() * 1000)
}
# Emit after a short delay to simulate async behavior
QTimer.singleShot(100, lambda: self.messageReceived.emit(json.dumps(background_message)))
return True
return False
@pyqtSlot(str, str, str, result=str)
def sendMessage(self, target_extension_id, message, options, callback=None):
"""Send a message to an extension."""
print(f"Sending message to {target_extension_id}: {message}")
# Parse the message and options
try:
message_obj = json.loads(message)
options_obj = json.loads(options)
# Emit the messageReceived signal to notify all listeners
# This will be received by any extension page that has registered a listener
self.messageReceived.emit(message)
# For our test extension, we'll simulate a response from the background script
if target_extension_id == "test-extension":
response = {
"success": True,
"from": "background",
"received": True,
"timestamp": int(__import__('time').time() * 1000),
"echo": message_obj
}
else:
# Default response for other extensions
response = {"success": True, "echo": message_obj}
if callback:
callback(json.dumps(response))
return json.dumps(response)
except Exception as e:
print(f"Error sending message: {e}")
if callback:
callback('{"error": "Failed to send message"}')
return '{"error": "Failed to send message"}'
@pyqtSlot(str, str)
def connectPort(self, extension_id, port_name):
"""Connect a port for message passing."""
print(f"Connecting port {port_name} for extension {extension_id}")
if extension_id not in self.extension_ports:
self.extension_ports[extension_id] = {}
self.extension_ports[extension_id][port_name] = True
# For test extension, send an initial connection message
if extension_id == "test-extension":
# Send a welcome message after a short delay to simulate async behavior
QTimer.singleShot(100, lambda: self.send_port_welcome_message(extension_id, port_name))
def send_port_welcome_message(self, extension_id, port_name):
"""Send a welcome message to a newly connected port."""
welcome_message = {
"action": "connected",
"from": "background",
"timestamp": int(__import__('time').time() * 1000),
"message": f"Welcome to port {port_name}!"
}
self.portMessageReceived.emit(port_name, json.dumps(welcome_message))
@pyqtSlot(str, str)
def disconnectPort(self, extension_id, port_name):
"""Disconnect a port."""
print(f"Disconnecting port {port_name} for extension {extension_id}")
if extension_id in self.extension_ports and port_name in self.extension_ports[extension_id]:
del self.extension_ports[extension_id][port_name]
@pyqtSlot(str, str, str)
def postPortMessage(self, extension_id, port_name, message):
"""Post a message to a port."""
print(f"Posting message to port {port_name} for extension {extension_id}: {message}")
# Check if the port exists
if extension_id in self.extension_ports and port_name in self.extension_ports[extension_id]:
# For test extension, echo the message back with additional data
if extension_id == "test-extension":
try:
message_obj = json.loads(message)
response = {
"received": True,
"from": "background",
"timestamp": int(__import__('time').time() * 1000),
"echo": message_obj
}
# Emit the response after a short delay to simulate async processing
QTimer.singleShot(200, lambda: self.portMessageReceived.emit(port_name, json.dumps(response)))
except Exception as e:
print(f"Error processing port message: {e}")
self.portMessageReceived.emit(port_name, json.dumps({"error": str(e)}))
else:
# For other extensions, just echo the message back
self.portMessageReceived.emit(port_name, message)
else:
print(f"Port {port_name} for extension {extension_id} not found")
@pyqtSlot(str, str)
def sendResponse(self, extension_id, response):
"""Send a response to a message."""
print(f"Sending response for extension {extension_id}: {response}")
# In a real implementation, we would route this response to the appropriate sender
# For now, we'll just print it
@pyqtSlot(str)
def reloadExtension(self, extension_id):
"""Reload an extension."""
print(f"Reloading extension {extension_id}")
# In a real implementation, we would reload the extension
# For now, we'll just print a message
# If the browser has a reload_profile_and_tabs method, call it
if hasattr(self.browser, 'reload_profile_and_tabs'):
self.browser.reload_profile_and_tabs()
def create_runtime_api_script():
"""Create a QWebEngineScript to inject the chrome.runtime API emulation."""
script = QWebEngineScript()
# Load the QWebChannel.js script
qwebchannel_path = os.path.join(os.path.dirname(__file__), 'qwebchannel.js')
with open(qwebchannel_path, 'r', encoding='utf-8') as f:
qwebchannel_js = f.read()
# Load the chrome-runtime-api.js script
runtime_api_path = os.path.join(os.path.dirname(__file__), 'chrome-runtime-api.js')
with open(runtime_api_path, 'r', encoding='utf-8') as f:
runtime_api_js = f.read()
# Create a wrapper script that ensures QWebChannel is loaded before chrome-runtime-api.js
wrapper_js = """
// Wrapper to ensure proper initialization sequence
(function() {
// Create global initialization tracking variables
window.__qtWebChannelTransportReady = false;
window.__chromeRuntimeInitialized = false;
window.__initializationAttempts = 0;
// First, make sure QWebChannel is available
%s
// Add helper functions to check initialization status
window.isQWebChannelReady = function() {
return typeof QWebChannel !== 'undefined' &&
typeof qt !== 'undefined' &&
qt.webChannelTransport !== undefined;
};
window.isChromeRuntimeInitialized = function() {
return window.__chromeRuntimeInitialized === true ||
(typeof chrome !== 'undefined' &&
typeof chrome.runtime !== 'undefined' &&
window.__qtWebChannelTransportReady === true);
};
// Function to handle initialization failure
window.reportInitializationStatus = function() {
console.log('Initialization Status:');
console.log('- QWebChannel available: ' + (typeof QWebChannel !== 'undefined'));
console.log('- qt object available: ' + (typeof qt !== 'undefined'));
console.log('- qt.webChannelTransport available: ' + (typeof qt !== 'undefined' && qt.webChannelTransport !== undefined));
console.log('- chrome object available: ' + (typeof chrome !== 'undefined'));
console.log('- chrome.runtime available: ' + (typeof chrome !== 'undefined' && chrome.runtime !== undefined));
console.log('- __qtWebChannelTransportReady: ' + window.__qtWebChannelTransportReady);
console.log('- __chromeRuntimeInitialized: ' + window.__chromeRuntimeInitialized);
console.log('- __initializationAttempts: ' + window.__initializationAttempts);
};
// Set up event listeners for initialization events
document.addEventListener('webChannelReady', function(event) {
console.log('webChannelReady event received');
window.__qtWebChannelTransportReady = true;
window.reportInitializationStatus();
});
document.addEventListener('chrome.runtime.initialized', function(event) {
console.log('chrome.runtime.initialized event received for extension: ' + event.detail.extensionId);
window.__chromeRuntimeInitialized = true;
window.reportInitializationStatus();
});
// Then initialize the chrome.runtime API
%s
// Log the initialization status
console.log('Runtime API script injected. QWebChannel ready: ' + window.isQWebChannelReady());
window.reportInitializationStatus();
// Set up a periodic check for initialization status
var statusCheckInterval = setInterval(function() {
window.__initializationAttempts++;
if (window.isChromeRuntimeInitialized()) {
console.log('Chrome runtime API successfully initialized');
clearInterval(statusCheckInterval);
} else if (window.__initializationAttempts >= 50) {
console.error('Failed to initialize Chrome runtime API after 50 attempts');
window.reportInitializationStatus();
clearInterval(statusCheckInterval);
} else if (window.__initializationAttempts %% 10 === 0) {
console.log('Still waiting for Chrome runtime API initialization (attempt ' + window.__initializationAttempts + ')');
window.reportInitializationStatus();
// Try to trigger initialization if not already initialized
if (!window.__qtWebChannelTransportReady) {
console.log('Attempting to trigger initialization');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
}
}, 500);
})();
""" % (qwebchannel_js, runtime_api_js)
script.setSourceCode(wrapper_js)
script.setName("chrome-runtime-api")
script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld)
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation)
script.setRunsOnSubFrames(True)
return script
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<title>Chrome Runtime API Test</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
}
.container {
background-color: white;
border-radius: 5px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
button {
background-color: #4285f4;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #3367d6;
}
pre {
background-color: #f0f0f0;
padding: 10px;
border-radius: 4px;
overflow: auto;
max-height: 300px;
}
.output {
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Chrome Runtime API Test</h1>
<div>
<button id="testGetURL">Test chrome.runtime.getURL</button>
<button id="testGetManifest">Test chrome.runtime.getManifest</button>
<button id="testSendMessage">Test chrome.runtime.sendMessage</button>
<button id="testConnect">Test chrome.runtime.connect</button>
<button id="checkStatus" style="background-color: #ff9800;">Check Initialization Status</button>
</div>
<div class="output">
<h3>Output:</h3>
<pre id="output">Results will appear here...</pre>
</div>
</div>
<script>
// Helper function to log output
function log(message) {
const output = document.getElementById('output');
output.textContent += message + '\n';
console.log(message);
}
// Helper function to stringify objects
function stringify(obj) {
return JSON.stringify(obj, null, 2);
}
// Function to check if chrome.runtime is initialized
function isChromeRuntimeReady() {
// Check for our global initialization flags first
if (typeof window.__qtWebChannelTransportReady === 'boolean' &&
window.__qtWebChannelTransportReady === true) {
return true;
}
// Check if the isChromeRuntimeInitialized function exists and use it
if (typeof window.isChromeRuntimeInitialized === 'function') {
return window.isChromeRuntimeInitialized();
}
// Fallback to basic checks
return typeof chrome !== 'undefined' &&
chrome.runtime &&
typeof chrome.runtime.id === 'string';
}
// Function to wait for chrome.runtime to be initialized
function waitForChromeRuntime(callback, maxAttempts = 100) {
let attempts = 0;
function checkRuntime() {
if (isChromeRuntimeReady()) {
log('chrome.runtime API is ready');
callback();
} else if (attempts < maxAttempts) {
attempts++;
// Log less frequently to reduce output spam
if (attempts === 1 || attempts % 10 === 0) {
log(`Waiting for chrome.runtime API to initialize (attempt ${attempts}/${maxAttempts})...`);
}
// Try to trigger initialization if we're past several attempts
if (attempts > 20 && attempts % 20 === 0) {
log('Attempting to trigger chrome.runtime initialization');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
setTimeout(checkRuntime, 100);
} else {
log('ERROR: chrome.runtime API failed to initialize after multiple attempts');
// Report initialization status if available
if (typeof window.reportInitializationStatus === 'function') {
log('Detailed initialization status:');
window.reportInitializationStatus();
} else {
log('Initialization status:');
log(`- chrome defined: ${typeof chrome !== 'undefined'}`);
log(`- chrome.runtime defined: ${typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined'}`);
log(`- __qtWebChannelTransportReady: ${window.__qtWebChannelTransportReady}`);
}
}
}
checkRuntime();
}
// Add a listener for the chrome.runtime.initialized event
document.addEventListener('chrome.runtime.initialized', (event) => {
log(`chrome.runtime API initialized for extension: ${event.detail.extensionId}`);
});
// Test chrome.runtime.getURL
document.getElementById('testGetURL').addEventListener('click', function() {
waitForChromeRuntime(() => {
try {
const url = chrome.runtime.getURL('test.html');
log('chrome.runtime.getURL("test.html") => ' + url);
} catch (e) {
log('ERROR: ' + e.message);
}
});
});
// Test chrome.runtime.getManifest
document.getElementById('testGetManifest').addEventListener('click', function() {
waitForChromeRuntime(() => {
try {
const manifest = chrome.runtime.getManifest();
log('chrome.runtime.getManifest() => ' + stringify(manifest));
} catch (e) {
log('ERROR: ' + e.message);
}
});
});
// Test chrome.runtime.sendMessage
document.getElementById('testSendMessage').addEventListener('click', function() {
waitForChromeRuntime(() => {
try {
chrome.runtime.sendMessage(
{ greeting: 'hello', from: 'test page' },
function(response) {
log('chrome.runtime.sendMessage response => ' + stringify(response));
}
);
log('chrome.runtime.sendMessage sent');
} catch (e) {
log('ERROR: ' + e.message);
}
});
});
// Test chrome.runtime.connect
document.getElementById('testConnect').addEventListener('click', function() {
waitForChromeRuntime(() => {
try {
const port = chrome.runtime.connect({ name: 'test-port' });
log('chrome.runtime.connect created port: ' + port.name);
port.onMessage.addListener(function(message) {
log('Port received message: ' + stringify(message));
});
port.postMessage({ greeting: 'hello from port', from: 'test page' });
log('Port message sent');
// Disconnect after 2 seconds
setTimeout(function() {
port.disconnect();
log('Port disconnected');
}, 2000);
} catch (e) {
log('ERROR: ' + e.message);
}
});
});
// Check initialization status
document.getElementById('checkStatus').addEventListener('click', function() {
log('Checking chrome.runtime initialization status...');
if (typeof window.reportInitializationStatus === 'function') {
log('Detailed initialization status:');
window.reportInitializationStatus();
} else {
log('Initialization status:');
log(`- chrome defined: ${typeof chrome !== 'undefined'}`);
log(`- chrome.runtime defined: ${typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined'}`);
log(`- chrome.runtime.id: ${typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined' ? chrome.runtime.id : 'undefined'}`);
log(`- __qtWebChannelTransportReady: ${window.__qtWebChannelTransportReady}`);
log(`- __chromeRuntimeInitialized: ${window.__chromeRuntimeInitialized}`);
log(`- __initializationAttempts: ${window.__initializationAttempts}`);
log(`- QWebChannel available: ${typeof QWebChannel !== 'undefined'}`);
log(`- qt object available: ${typeof qt !== 'undefined'}`);
log(`- qt.webChannelTransport available: ${typeof qt !== 'undefined' && qt.webChannelTransport !== undefined}`);
// Try to trigger initialization
log('Attempting to trigger initialization...');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
});
// Check if chrome.runtime API is available on page load
window.addEventListener('DOMContentLoaded', function() {
if (isChromeRuntimeReady()) {
log('chrome.runtime API is already available');
log('Extension ID: ' + chrome.runtime.id);
} else {
log('Waiting for chrome.runtime API to initialize...');
}
});
</script>
</body>
</html>
\ No newline at end of file
{"cookies": [], "origins": []}
\ No newline at end of file
{"cookies": [], "origins": []}
\ No newline at end of file
{"cookies": [], "origins": []}
\ No newline at end of file
{"cookies": [], "origins": []}
\ No newline at end of file
{"cookies": [], "origins": []}
\ No newline at end of file
{"cookies": [], "origins": []}
\ No newline at end of file
{"cookies": [], "origins": []}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
qbrowser.py - A Qt6-based browser that wraps and extends the Playwright browser class
This module provides a Qt6-based browser interface that wraps the Playwright browser
API and launches Chromium browser instances without decorations using the --app flag.
Each new page is opened as a new tab in the Qt6 window, and the interface includes
URL input, home button, and browser extension management.
"""
import os
import sys
import subprocess
import tempfile
import json
import shutil
import asyncio
import platform
import signal
from pathlib import Path
from typing import Dict, List, Optional, Union, Callable, Any
import configparser
from functools import partial
from PyQt6.QtCore import Qt, QUrl, QProcess, pyqtSignal, QSize, QRect, QTimer, QMimeData, QPoint, QByteArray, QBuffer
from PyQt6.QtGui import QIcon, QAction, QKeySequence, QFont, QColor, QPainter, QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTabWidget, QToolBar, QLineEdit,
QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QDialog,
QLabel, QListWidget, QListWidgetItem, QCheckBox, QMenu,
QSizePolicy, QStyle, QFrame, QSplitter, QMessageBox, QStatusBar, QTabBar, QStyleOptionTab, QFileDialog,
QInputDialog, QTextEdit
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import (
QWebEngineProfile, QWebEngineSettings, QWebEnginePage, QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob, QWebEngineUrlRequestInterceptor, QWebEngineUrlScheme
)
import mimetypes
# Import Playwright for API compatibility
from playwright.async_api import async_playwright, Browser as PlaywrightBrowser
from playwright.async_api import Page as PlaywrightPage
from playwright.async_api import BrowserContext as PlaywrightBrowserContext
class ChromeWebEnginePage(QWebEnginePage):
"""
Custom QWebEnginePage that allows navigation to chrome:// URLs
"""
def certificateError(self, error):
# Accept all certificates to allow chrome:// URLs
return True
def javaScriptConsoleMessage(self, level, message, line, source):
# Log JavaScript console messages
print(f"JS Console ({source}:{line}): {message}")
def acceptNavigationRequest(self, url, type, isMainFrame):
# Always accept chrome:// URLs
if url.scheme() == "chrome":
print(f"Accepting chrome:// URL navigation: {url.toString()}")
return True
return super().acceptNavigationRequest(url, type, isMainFrame)
def createWindow(self, windowType):
"""Create a new window when requested, especially for chrome:// URLs"""
# Get the main browser window
browser = self.parent().window()
if isinstance(browser, Browser):
# Create a new tab with our custom page
tab = browser.new_page("about:blank")
if hasattr(tab, 'web_view'):
# Ensure it's using our custom page class
if not isinstance(tab.web_view.page(), ChromeWebEnginePage):
page = ChromeWebEnginePage(browser.profile, tab.web_view)
tab.web_view.setPage(page)
return tab.web_view.page()
# Fallback: create a default page
return ChromeWebEnginePage(self.profile())
def javaScriptAlert(self, securityOrigin, msg):
"""Handle JavaScript alerts, especially from chrome:// URLs"""
print(f"JS Alert from {securityOrigin.toString()}: {msg}")
# Allow alerts from chrome:// URLs to pass through
return super().javaScriptAlert(securityOrigin, msg)
def javaScriptConfirm(self, securityOrigin, msg):
"""Handle JavaScript confirms, especially from chrome:// URLs"""
print(f"JS Confirm from {securityOrigin.toString()}: {msg}")
# Allow confirms from chrome:// URLs to pass through
return super().javaScriptConfirm(securityOrigin, msg)
def urlChanged(self, url):
"""Handle URL changes, especially to chrome:// URLs"""
if url.scheme() == "chrome":
print(f"URL changed to chrome:// URL: {url.toString()}")
super().urlChanged(url)
class BrowserTab(QWidget):
"""
A tab widget that contains a web view and manages a Chromium process.
"""
urlChanged = pyqtSignal(str)
titleChanged = pyqtSignal(str)
loadStatusChanged = pyqtSignal()
def __init__(self, parent=None, url="about:blank", profile=None):
super().__init__(parent)
self.parent = parent
self.url = url
self.profile = profile
self.process = None
self.page = None # Playwright page object
self.is_loading = False
# Create a container for the web view
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Create a placeholder widget until the Chromium process is ready
self.placeholder = QLabel("Loading browser...")
self.placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.layout.addWidget(self.placeholder)
# Start the Chromium process
self.start_browser()
def start_browser(self):
"""Initialize the QWebEngineView for browsing."""
# Create a QWebEngineView for the Qt UI
# This will be the actual browser, no separate Chromium process
self.web_view = QWebEngineView()
self.web_view.setEnabled(True) # Enable interaction with this view
# Set up a custom profile for this tab with our ChromeWebEnginePage that allows chrome:// URLs
page = ChromeWebEnginePage(self.profile, self.web_view)
self.web_view.setPage(page)
# Connect signals
self.web_view.loadStarted.connect(self.on_load_started)
self.web_view.loadFinished.connect(self.on_load_finished)
self.web_view.urlChanged.connect(lambda url: self.urlChanged.emit(url.toString()))
self.web_view.titleChanged.connect(lambda title: self.titleChanged.emit(title))
# Load the URL
self.web_view.load(QUrl(self.url))
# Replace the placeholder with the web view
self.layout.removeWidget(self.placeholder)
self.placeholder.deleteLater()
self.layout.addWidget(self.web_view)
print(f"Loading URL in QWebEngineView: {self.url}")
def navigate(self, url):
"""Navigate to the specified URL."""
self.url = url
if hasattr(self, 'web_view'):
print(f"Navigating to: {url}")
# Special handling for chrome:// URLs
if url.startswith("chrome://"):
self.navigate_to_chrome_url(url)
else:
self.web_view.load(QUrl(url))
# If we have a Playwright page, navigate it as well for API compatibility
if self.page:
asyncio.create_task(self.page.goto(url))
def navigate_to_chrome_url(self, url):
"""Special handling for chrome:// URLs at the tab level."""
print(f"Tab-level chrome:// URL navigation: {url}")
# Ensure we're using the ChromeWebEnginePage
if not isinstance(self.web_view.page(), ChromeWebEnginePage):
# Get the browser window to access the profile
browser = self.parent.window()
if isinstance(browser, Browser):
page = ChromeWebEnginePage(browser.profile, self.web_view)
self.web_view.setPage(page)
# Now navigate to the chrome:// URL
self.web_view.load(QUrl(url))
def on_load_started(self):
self.is_loading = True
self.loadStatusChanged.emit()
def on_load_finished(self, success):
"""Handle the page load finishing."""
self.is_loading = False
self.loadStatusChanged.emit()
if success:
print(f"Page loaded successfully: {self.web_view.url().toString()}")
# Update the title if available
title = self.web_view.title()
if title:
self.titleChanged.emit(title)
else:
print(f"Failed to load page: {self.url}")
# We no longer need process handling methods since we're using QWebEngineView directly
def close(self):
"""Close the tab."""
# Close the web view
if hasattr(self, 'web_view'):
self.web_view.close()
super().close()
class ChromeUrlInterceptor(QWebEngineUrlRequestInterceptor):
"""
Intercepts URL requests to handle chrome:// URLs specially
"""
def __init__(self, browser):
super().__init__()
self.browser = browser
def interceptRequest(self, info):
url = info.requestUrl()
if url.scheme() == "chrome":
print(f"Intercepted chrome:// URL: {url.toString()}")
# We don't block the request, just log it for debugging
# The actual handling is done by ChromeWebEnginePage
class ExtensionSchemeHandler(QWebEngineUrlSchemeHandler):
"""A custom URL scheme handler for loading extension files."""
def __init__(self, extensions_dir, parent=None):
super().__init__(parent)
self.extensions_dir = extensions_dir
self.jobs = {} # Keep track of active jobs and their buffers
def requestStarted(self, job: QWebEngineUrlRequestJob):
url = job.requestUrl()
ext_id = url.host()
resource_path = url.path().lstrip('/')
file_path = os.path.abspath(os.path.join(self.extensions_dir, ext_id, resource_path))
try:
with open(file_path, 'rb') as f:
content = f.read()
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type:
mime_type = mime_type.encode()
else:
mime_type = b'application/octet-stream'
buf = QBuffer(parent=self)
buf.setData(content)
buf.open(QBuffer.OpenModeFlag.ReadOnly)
# Store buffer to prevent garbage collection
self.jobs[job] = buf
# Reply with the content
job.reply(mime_type, buf)
print(f"Replied with content for: {file_path}")
except FileNotFoundError:
print(f"Extension resource not found: {file_path}")
job.fail(QWebEngineUrlRequestJob.Error.UrlNotFound)
if job in self.jobs:
del self.jobs[job]
except Exception as e:
print(f"Error loading extension resource {file_path}: {e}")
job.fail(QWebEngineUrlRequestJob.Error.RequestFailed)
if job in self.jobs:
del self.jobs[job]
class ExtensionDialog(QDialog):
"""
Dialog for managing browser extensions.
"""
def __init__(self, parent=None, extensions_dir=None):
super().__init__(parent)
self.setWindowTitle("Browser Extensions")
self.setMinimumSize(500, 400)
self.extensions_dir = extensions_dir
self.open_popups = []
# Create layout
layout = QVBoxLayout(self)
# Create extensions list
self.extensions_list = QListWidget()
layout.addWidget(self.extensions_list)
# Create buttons
button_layout = QHBoxLayout()
self.install_button = QPushButton("Install New")
self.remove_button = QPushButton("Remove")
self.enable_button = QPushButton("Enable/Disable")
self.popup_button = QPushButton("Open Popup")
self.close_button = QPushButton("Close")
button_layout.addWidget(self.install_button)
button_layout.addWidget(self.remove_button)
button_layout.addWidget(self.enable_button)
button_layout.addWidget(self.popup_button)
button_layout.addStretch()
button_layout.addWidget(self.close_button)
layout.addLayout(button_layout)
# Connect signals
self.close_button.clicked.connect(self.accept)
self.install_button.clicked.connect(self.install_extension)
self.remove_button.clicked.connect(self.remove_extension)
self.enable_button.clicked.connect(self.toggle_extension)
self.popup_button.clicked.connect(self.open_extension_popup)
# Load extensions
self.load_extensions()
def load_extensions(self):
"""Load and display installed extensions."""
self.extensions_list.clear()
if not self.extensions_dir or not os.path.exists(self.extensions_dir):
self.extensions_list.addItem("No extensions directory found")
return
# Find all extension directories
for ext_name in os.listdir(self.extensions_dir):
ext_path = os.path.join(self.extensions_dir, ext_name)
if os.path.isdir(ext_path):
enabled = not ext_name.endswith(".disabled")
base_name = ext_name[:-9] if not enabled else ext_name
manifest_path = os.path.join(ext_path, "manifest.json")
if os.path.exists(manifest_path):
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
name = manifest.get("name", base_name)
version = manifest.get("version", "unknown")
status = "ACTIVE" if enabled else "INACTIVE"
item = QListWidgetItem(f"{name} (v{version}) - {status}")
item.setData(Qt.ItemDataRole.UserRole, ext_path)
item.setCheckState(Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked)
self.extensions_list.addItem(item)
except Exception as e:
print(f"Error loading extension manifest for {ext_name}: {e}")
item = QListWidgetItem(f"{ext_name} (Error loading manifest)")
item.setData(Qt.ItemDataRole.UserRole, ext_path)
self.extensions_list.addItem(item)
def install_extension(self):
"""Install a new extension from a directory."""
source_dir = QFileDialog.getExistingDirectory(self, "Select Extension Directory")
if not source_dir:
return
ext_name = os.path.basename(source_dir)
dest_dir = os.path.join(self.extensions_dir, ext_name)
if os.path.exists(dest_dir):
QMessageBox.warning(self, "Extension Exists", f"An extension named '{ext_name}' already exists.")
return
try:
shutil.copytree(source_dir, dest_dir)
self.load_extensions()
QMessageBox.information(self, "Restart Required",
"The extension has been installed. Please restart the browser for it to be loaded.")
except Exception as e:
QMessageBox.critical(self, "Installation Error", f"Could not install the extension: {e}")
def remove_extension(self):
"""Remove the selected extension."""
selected_items = self.extensions_list.selectedItems()
if not selected_items:
return
for item in selected_items:
ext_path = item.data(Qt.ItemDataRole.UserRole)
if ext_path and os.path.exists(ext_path):
try:
shutil.rmtree(ext_path)
print(f"Removed extension: {ext_path}")
except Exception as e:
print(f"Error removing extension: {e}")
self.load_extensions()
def toggle_extension(self):
"""Toggle the enabled state of the selected extension and reload the profile."""
selected_items = self.extensions_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
ext_path = item.data(Qt.ItemDataRole.UserRole)
ext_name = os.path.basename(ext_path)
is_enabled = not ext_name.endswith(".disabled")
if is_enabled:
new_path = ext_path + ".disabled"
else:
new_path = ext_path[:-9]
try:
# Rename the folder to enable/disable the extension
os.rename(ext_path, new_path)
# Update the list in the dialog
self.load_extensions()
# Get the main browser window and trigger a profile reload
browser = self.parent()
if browser and hasattr(browser, 'reload_profile_and_tabs'):
browser.reload_profile_and_tabs()
else:
# This is a fallback in case the parent isn't the browser window
QMessageBox.information(self, "Restart Required",
"Change applied. Please restart the browser for it to take effect.")
except OSError as e:
QMessageBox.critical(self, "Error", f"Could not change extension state: {e}")
def open_extension_popup(self):
"""Open the popup of the selected extension, if it has one."""
selected_items = self.extensions_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
ext_path = item.data(Qt.ItemDataRole.UserRole)
ext_name = os.path.basename(ext_path)
if ext_name.endswith(".disabled"):
QMessageBox.warning(self, "Extension Disabled", "Cannot open popup for a disabled extension.")
return
manifest_path = os.path.join(ext_path, "manifest.json")
if not os.path.exists(manifest_path):
QMessageBox.warning(self, "Error", "manifest.json not found for this extension.")
return
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except Exception as e:
QMessageBox.critical(self, "Error", f"Could not read manifest.json: {e}")
return
popup_path = None
action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if action and action.get('default_popup'):
popup_path = action.get('default_popup')
if not popup_path:
QMessageBox.information(self, "No Popup", "This extension does not have a popup.")
return
popup_file = os.path.join(ext_path, popup_path)
if not os.path.exists(popup_file):
QMessageBox.warning(self, "Error", f"Popup file not found: {popup_path}")
return
# Open the popup in a new dialog
popup_dialog = QDialog(self)
popup_dialog.setWindowTitle(manifest.get("name", "Extension Popup"))
layout = QVBoxLayout(popup_dialog)
web_view = QWebEngineView()
browser_window = self.parent()
if not browser_window or not isinstance(browser_window, Browser):
QMessageBox.critical(self, "Error", "Could not get browser window reference.")
return
# Use the main browser's profile so the scheme handler is available
page = QWebEnginePage(browser_window.profile, web_view)
web_view.setPage(page)
popup_url = QUrl(f"qextension://{ext_name}/{popup_path}")
web_view.load(popup_url)
layout.addWidget(web_view)
popup_dialog.setLayout(layout)
popup_dialog.resize(400, 600)
popup_dialog.show()
self.open_popups.append(popup_dialog)
popup_dialog.finished.connect(lambda: self.open_popups.remove(popup_dialog))
class DetachableTabBar(QTabBar):
tabDetached = pyqtSignal(int, QPoint)
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptDrops(True)
self.drag_start_pos = QPoint()
self.drag_tab_index = -1
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.drag_start_pos = event.pos()
self.drag_tab_index = self.tabAt(self.drag_start_pos)
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if not (event.buttons() & Qt.MouseButton.LeftButton):
return super().mouseMoveEvent(event)
if self.drag_tab_index == -1:
return super().mouseMoveEvent(event)
tab_widget = self.parent().widget(self.drag_tab_index)
if tab_widget and hasattr(tab_widget, 'is_loading') and tab_widget.is_loading:
return # Don't allow dragging while the tab is loading
if (event.pos() - self.drag_start_pos).manhattanLength() < QApplication.startDragDistance():
return super().mouseMoveEvent(event)
drag = QDrag(self)
mime_data = QMimeData()
mime_data.setData("application/x-qbrowser-tab-index", QByteArray(str(self.drag_tab_index).encode()))
drag.setMimeData(mime_data)
# Use a simple dummy pixmap to avoid all painter errors
pixmap = QPixmap(100, 30)
pixmap.fill(QColor(53, 53, 53))
painter = QPainter(pixmap)
painter.setPen(Qt.GlobalColor.white)
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, self.tabText(self.drag_tab_index))
painter.end()
drag.setPixmap(pixmap)
tab_rect = self.tabRect(self.drag_tab_index)
if tab_rect.isValid():
drag.setHotSpot(event.pos() - tab_rect.topLeft())
else:
drag.setHotSpot(event.pos()) # Fallback hotspot
drop_action = drag.exec(Qt.DropAction.MoveAction)
if drop_action == Qt.DropAction.IgnoreAction:
self.tabDetached.emit(self.drag_tab_index, event.globalPosition().toPoint())
self.drag_tab_index = -1
super().mouseMoveEvent(event)
def dragEnterEvent(self, event):
if event.mimeData().hasFormat("application/x-qbrowser-tab-index"):
event.acceptProposedAction()
else:
super().dragEnterEvent(event)
def dropEvent(self, event):
if not event.mimeData().hasFormat("application/x-qbrowser-tab-index"):
return super().dropEvent(event)
source_tab_bar = event.source()
if not isinstance(source_tab_bar, DetachableTabBar):
return super().dropEvent(event)
source_widget = source_tab_bar.parent()
dest_widget = self.parent()
from_index = int(event.mimeData().data("application/x-qbrowser-tab-index"))
to_index = self.tabAt(event.position().toPoint())
if source_widget == dest_widget:
if to_index == -1:
to_index = self.count()
self.moveTab(from_index, to_index)
self.parent().setCurrentIndex(to_index)
event.acceptProposedAction()
return
# Cross-widget drop
tab_content = source_widget.widget(from_index)
tab_title = source_widget.tabText(from_index)
# Disconnect signals from the old browser
old_browser = tab_content.browser
if old_browser:
try:
tab_content.urlChanged.disconnect(old_browser.update_url)
tab_content.titleChanged.disconnect()
tab_content.loadStatusChanged.disconnect(old_browser.update_status_bar)
except (TypeError, RuntimeError):
pass
# Remove from source widget. The page widget is not deleted.
source_widget.removeTab(from_index)
if to_index == -1:
to_index = self.count()
# Add to destination widget
new_index = dest_widget.insertTab(to_index, tab_content, tab_title)
dest_widget.setCurrentIndex(new_index)
# Connect signals to the new browser
new_browser = dest_widget.window()
tab_content.browser = new_browser
tab_content.urlChanged.connect(new_browser.update_url)
tab_content.titleChanged.connect(lambda title, tab=tab_content: new_browser.update_title(title, tab))
tab_content.loadStatusChanged.connect(new_browser.update_status_bar)
# Close the source window if it has no more tabs
if source_widget.count() == 0:
source_widget.window().close()
event.setDropAction(Qt.DropAction.MoveAction)
event.accept()
class ProfileDialog(QDialog):
"""
Dialog for selecting or creating a browser profile.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Select Profile")
self.setMinimumWidth(400)
self.selected_profile_path = None
self.profiles_dir = "data"
if not os.path.exists(self.profiles_dir):
os.makedirs(self.profiles_dir)
layout = QVBoxLayout(self)
layout.addWidget(QLabel("Select a profile to launch:"))
self.profile_list = QListWidget()
self.profile_list.itemDoubleClicked.connect(self.accept_selection)
layout.addWidget(self.profile_list)
button_layout = QHBoxLayout()
self.new_button = QPushButton("Create New")
self.new_button.clicked.connect(self.create_new_profile)
button_layout.addWidget(self.new_button)
self.delete_button = QPushButton("Delete")
self.delete_button.clicked.connect(self.delete_profile)
button_layout.addWidget(self.delete_button)
button_layout.addStretch()
self.select_button = QPushButton("Launch")
self.select_button.setDefault(True)
self.select_button.clicked.connect(self.accept_selection)
button_layout.addWidget(self.select_button)
layout.addLayout(button_layout)
self.load_profiles()
self.update_button_states()
def update_button_states(self):
has_selection = len(self.profile_list.selectedItems()) > 0
self.select_button.setEnabled(has_selection)
self.delete_button.setEnabled(has_selection)
def load_profiles(self):
self.profile_list.clear()
profiles = []
for item in os.listdir(self.profiles_dir):
if os.path.isdir(os.path.join(self.profiles_dir, item)) and item.startswith("browser_profile_"):
profiles.append(item)
for profile_dir_name in sorted(profiles):
profile_name = profile_dir_name.replace("browser_profile_", "")
list_item = QListWidgetItem(profile_name)
list_item.setData(Qt.ItemDataRole.UserRole, os.path.join(self.profiles_dir, profile_dir_name))
self.profile_list.addItem(list_item)
if self.profile_list.count() > 0:
self.profile_list.setCurrentRow(0)
self.profile_list.itemSelectionChanged.connect(self.update_button_states)
self.update_button_states()
def create_new_profile(self):
while True:
profile_name, ok = QInputDialog.getText(self, "Create New Profile", "Enter a name for the new profile:")
if not ok:
return # User cancelled
if not profile_name.strip():
QMessageBox.warning(self, "Invalid Name", "Profile name cannot be empty.")
continue
profile_path = os.path.join(self.profiles_dir, f"browser_profile_{profile_name.strip()}")
if not os.path.exists(profile_path):
try:
os.makedirs(profile_path)
self._install_default_extensions(profile_path)
self.load_profiles()
# Select the new profile
for i in range(self.profile_list.count()):
if self.profile_list.item(i).text() == profile_name.strip():
self.profile_list.setCurrentRow(i)
break
return
except OSError as e:
QMessageBox.critical(self, "Error", f"Could not create profile directory: {e}")
return
else:
QMessageBox.warning(self, "Profile Exists", "A profile with that name already exists.")
def delete_profile(self):
selected_items = self.profile_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
profile_name = item.text()
profile_path = item.data(Qt.ItemDataRole.UserRole)
reply = QMessageBox.question(self, "Confirm Deletion", f"Are you sure you want to delete the profile '{profile_name}'?\nThis will permanently delete all its data.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
try:
shutil.rmtree(profile_path)
self.load_profiles()
self.update_button_states()
except Exception as e:
QMessageBox.critical(self, "Error", f"Could not delete profile: {e}")
def accept_selection(self):
selected_items = self.profile_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
self.selected_profile_path = item.data(Qt.ItemDataRole.UserRole)
self.accept()
@staticmethod
def _install_default_extensions(profile_path):
"""Copies default extensions from assets to a new profile directory."""
source_extensions_dir = "assets/browser/extensions"
if not os.path.isdir(source_extensions_dir):
print(f"Default extensions directory not found: {source_extensions_dir}")
return
dest_extensions_dir = os.path.join(profile_path, "Default", "Extensions")
try:
os.makedirs(dest_extensions_dir, exist_ok=True)
for item_name in os.listdir(source_extensions_dir):
source_item_path = os.path.join(source_extensions_dir, item_name)
dest_item_path = os.path.join(dest_extensions_dir, item_name)
if os.path.isdir(source_item_path):
if not os.path.exists(dest_item_path):
shutil.copytree(source_item_path, dest_item_path)
print(f"Installed default extension '{item_name}' to profile.")
except (OSError, IOError) as e:
QMessageBox.warning(None, "Extension Installation Error",
f"Could not install default extensions: {e}")
@staticmethod
def get_profile_path(parent=None):
profiles_dir = "data"
if not os.path.exists(profiles_dir):
os.makedirs(profiles_dir)
profiles = [p for p in os.listdir(profiles_dir) if p.startswith("browser_profile_")]
if not profiles:
# First run experience: must create a profile to continue.
while True:
profile_name, ok = QInputDialog.getText(parent, "Create First Profile", "Welcome! Please create a profile to begin:")
if not ok:
return None # User cancelled initial creation, so exit.
if not profile_name.strip():
QMessageBox.warning(parent, "Invalid Name", "Profile name cannot be empty.")
continue
profile_path = os.path.join(profiles_dir, f"browser_profile_{profile_name.strip()}")
if os.path.exists(profile_path):
QMessageBox.warning(parent, "Profile Exists", "A profile with that name already exists. Please choose another name.")
continue
try:
os.makedirs(profile_path)
ProfileDialog._install_default_extensions(profile_path)
return profile_path # Success!
except OSError as e:
QMessageBox.critical(parent, "Error", f"Could not create profile directory: {e}")
return None
else:
# Profiles exist, show the manager dialog.
dialog = ProfileDialog(parent)
if dialog.exec() == QDialog.DialogCode.Accepted:
return dialog.selected_profile_path
else:
return None # User closed the manager.
class Browser(QMainWindow):
"""
Main browser window that wraps the Playwright browser class.
"""
instances = []
def __init__(self, initial_url=None, debug=False, detached_tab=None, profile_path=None):
super().__init__()
Browser.instances.append(self)
self.setWindowTitle("SHMCamStudio Browser")
self.setWindowIcon(QIcon('assets/logo.jpg'))
self.resize(1024, 768)
# Create a central widget and layout
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QVBoxLayout(self.central_widget)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Create toolbar
self.toolbar = QToolBar()
self.toolbar.setMovable(False)
self.toolbar.setIconSize(QSize(24, 24))
# Add back button
self.back_button = QPushButton()
self.back_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_ArrowBack))
self.back_button.setToolTip("Back")
self.back_button.clicked.connect(self.navigate_back)
self.toolbar.addWidget(self.back_button)
# Add forward button
self.forward_button = QPushButton()
self.forward_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_ArrowForward))
self.forward_button.setToolTip("Forward")
self.forward_button.clicked.connect(self.navigate_forward)
self.toolbar.addWidget(self.forward_button)
# Add reload button
self.reload_button = QPushButton()
self.reload_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_BrowserReload))
self.reload_button.setToolTip("Reload (F5)")
self.reload_button.clicked.connect(self.reload_page)
self.toolbar.addWidget(self.reload_button)
# Add home button
self.home_button = QPushButton()
self.home_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_DirHomeIcon))
self.home_button.setToolTip("Home")
self.home_button.clicked.connect(self.navigate_home)
self.toolbar.addWidget(self.home_button)
# Add URL input
self.url_input = QLineEdit()
self.url_input.setPlaceholderText("Enter URL...")
self.url_input.returnPressed.connect(self.navigate_to_url)
self.toolbar.addWidget(self.url_input)
# The extension buttons will be inserted before this one, so we need its action.
self.extensions_button = QPushButton()
self.extensions_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_FileDialogDetailedView))
self.extensions_button.setToolTip("Extensions")
self.extensions_button.clicked.connect(self.show_extensions)
self.extensions_button_action = self.toolbar.addWidget(self.extensions_button)
# Add Chrome URLs button with dropdown menu
self.chrome_button = QPushButton()
self.chrome_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_TitleBarMenuButton))
self.chrome_button.setToolTip("Chrome URLs")
self.chrome_menu = QMenu(self)
# Add common chrome:// URLs to the menu
chrome_urls = {
"Extensions": "chrome://extensions/",
"Settings": "chrome://settings/",
"History": "chrome://history/",
"Bookmarks": "chrome://bookmarks/",
"Downloads": "chrome://downloads/",
"Flags": "chrome://flags/",
"Version": "chrome://version/",
"GPU": "chrome://gpu/",
"Network": "chrome://net-internals/",
"Inspect": "chrome://inspect/",
"Media": "chrome://media-internals/",
"Components": "chrome://components/",
"System": "chrome://system/",
"Chrome URLs": "chrome://chrome-urls/" # This shows all available chrome:// URLs
}
# Add a separator and debug options
self.chrome_menu.addSeparator()
debug_action = self.chrome_menu.addAction("Debug Chrome URLs")
debug_action.triggered.connect(self.debug_chrome_urls)
for name, url in chrome_urls.items():
action = self.chrome_menu.addAction(name)
action.triggered.connect(lambda checked, u=url: self.navigate_to_chrome_url(u))
self.chrome_button.setMenu(self.chrome_menu)
self.toolbar.addWidget(self.chrome_button)
# Add new window button
self.new_window_button = QPushButton()
self.new_window_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_FileIcon))
self.new_window_button.setToolTip("New Window (Ctrl+W)")
self.new_window_button.clicked.connect(self.open_new_window)
self.toolbar.addWidget(self.new_window_button)
if debug:
self.debug_button = QPushButton()
self.debug_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_MessageBoxWarning))
self.debug_button.setToolTip("Toggle Developer Tools")
self.debug_button.clicked.connect(self.toggle_devtools)
self.toolbar.addWidget(self.debug_button)
self.layout.addWidget(self.toolbar)
# Create tab widget
self.tabs = QTabWidget()
self.tab_bar = DetachableTabBar()
self.tabs.setTabBar(self.tab_bar)
self.tab_bar.tabDetached.connect(self.handle_tab_detached)
self.tabs.setTabsClosable(True)
# self.tabs.setMovable(True) # This is handled by our DetachableTabBar
self.tabs.setDocumentMode(True)
self.tabs.tabCloseRequested.connect(self.close_tab)
self.tabs.currentChanged.connect(self.tab_changed)
self.layout.addWidget(self.tabs)
# Add status bar
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
# Add a new tab button
self.tabs.setCornerWidget(self._create_new_tab_button(), Qt.Corner.TopRightCorner)
# Set the home URL from config
config = configparser.ConfigParser()
config.read('shmcamstudio.conf')
self.home_url = config.get('Browser', 'home_url', fallback='https://www.google.com')
# Centralized browser profile
if not profile_path:
# This should not happen if main is used, but as a fallback
profile_path = "data/browser_profile_default"
self.profile_dir = profile_path
if not os.path.exists(self.profile_dir):
os.makedirs(self.profile_dir)
# Extensions must be in a specific subdirectory of the profile
self.extensions_dir = os.path.join(self.profile_dir, "Default", "Extensions")
if not os.path.exists(self.extensions_dir):
os.makedirs(self.extensions_dir)
self.profile = QWebEngineProfile(self.profile_dir, self)
# Enable extension developer mode to allow loading unpacked extensions
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanPaste, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.WebGLEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.ErrorPageEnabled, False)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
# Enable access to chrome:// URLs and other special URLs
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
# Additional settings to ensure chrome:// URLs work
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.ErrorPageEnabled, False)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled, True)
# Set developer mode flag in preferences
self.profile.setHttpUserAgent(self.profile.httpUserAgent() + " ChromiumExtensionDevMode")
# Enable Autofill feature for QWebEngine (using preferences instead of WebAttribute)
# Note: AutoFillEnabled attribute is not available in this PyQt6 version
# Create a directory for extension preferences if it doesn't exist
prefs_dir = os.path.join(self.profile_dir, "Default")
if not os.path.exists(prefs_dir):
os.makedirs(prefs_dir)
# Create or update the Preferences file to enable extension developer mode
prefs_file = os.path.join(prefs_dir, "Preferences")
prefs_data = {}
if os.path.exists(prefs_file):
try:
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
except json.JSONDecodeError:
# If the file exists but is invalid JSON, start with an empty dict
pass
# Set extension developer mode preferences
if 'extensions' not in prefs_data:
prefs_data['extensions'] = {}
prefs_data['extensions']['developer_mode'] = True
# Enable unpacked extensions and user scripts
if 'extensions' in prefs_data:
# Allow loading unpacked extensions
prefs_data['extensions']['allow_file_access'] = True
# Allow user scripts (required by some extensions like Lovense)
prefs_data['extensions']['allow_user_scripts'] = True
# Enable Autofill in preferences
if 'autofill' not in prefs_data:
prefs_data['autofill'] = {}
prefs_data['autofill']['enabled'] = True
# Enable additional features for extensions
if 'browser' not in prefs_data:
prefs_data['browser'] = {}
if 'enabled_labs_experiments' not in prefs_data['browser']:
prefs_data['browser']['enabled_labs_experiments'] = []
# Add Autofill experiments if not already present
autofill_experiments = [
"enable-autofill-credit-card-upload",
"enable-autofill-credit-card-authentication",
"enable-autofill-address-save-prompts",
"enable-experimental-web-platform-features"
]
for experiment in autofill_experiments:
if experiment not in prefs_data['browser']['enabled_labs_experiments']:
prefs_data['browser']['enabled_labs_experiments'].append(experiment)
# Write the updated preferences back to the file
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# Check for Lovense extension and configure it specifically
lovense_dir = os.path.join(self.extensions_dir, "lovense")
if os.path.exists(lovense_dir):
print("Lovense extension found, applying special configuration...")
# Create secure preferences file if it doesn't exist
secure_prefs_file = os.path.join(self.profile_dir, "Default", "Secure Preferences")
secure_prefs_data = {}
if os.path.exists(secure_prefs_file):
try:
with open(secure_prefs_file, 'r') as f:
secure_prefs_data = json.load(f)
except:
secure_prefs_data = {}
# Ensure extensions section exists
if 'extensions' not in secure_prefs_data:
secure_prefs_data['extensions'] = {}
# Add settings for Lovense
if 'settings' not in secure_prefs_data['extensions']:
secure_prefs_data['extensions']['settings'] = {}
# Set Lovense as trusted
secure_prefs_data['extensions']['settings']["lovense"] = {
"ack_external_install_prompt": True,
"installed_by_default": False,
"installed_by_oem": False,
"installed_by_policy": False,
"mark_acknowledged_external_install": True,
"was_installed_by_enterprise_policy": False
}
# Write secure preferences
with open(secure_prefs_file, 'w') as f:
json.dump(secure_prefs_data, f, indent=2)
# Create Local State file with extension settings
local_state_file = os.path.join(self.profile_dir, "Local State")
local_state_data = {}
if os.path.exists(local_state_file):
try:
with open(local_state_file, 'r') as f:
local_state_data = json.load(f)
except:
local_state_data = {}
# Ensure extensions section exists
if 'extensions' not in local_state_data:
local_state_data['extensions'] = {}
# Enable developer mode in local state
local_state_data['extensions']['ui'] = {
"developer_mode": True
}
# Write local state
with open(local_state_file, 'w') as f:
json.dump(local_state_data, f, indent=2)
print("Lovense extension configuration complete")
# Install URL scheme handlers
self.extension_scheme_handler = ExtensionSchemeHandler(self.extensions_dir)
# Reinstall URL scheme handlers
self.profile.installUrlSchemeHandler(b"qextension", self.extension_scheme_handler)
# Re-register chrome:// protocol with Qt
QWebEngineProfile.defaultProfile().setUrlRequestInterceptor(
ChromeUrlInterceptor(self)
)
# Register chrome:// protocol with Qt
QWebEngineProfile.defaultProfile().setUrlRequestInterceptor(
ChromeUrlInterceptor(self)
)
if detached_tab:
widget, title = detached_tab
index = self.tabs.addTab(widget, title)
self.tabs.setCurrentIndex(index)
widget.browser = self
widget.urlChanged.connect(self.update_url)
widget.titleChanged.connect(lambda t, tab=widget: self.update_title(t, tab))
widget.loadStatusChanged.connect(self.update_status_bar)
if hasattr(widget, 'web_view'):
self.update_title(widget.web_view.title(), widget)
self.update_url(widget.web_view.url().toString())
else:
# Create a new tab on startup
url_to_open = initial_url if initial_url is not None else self.home_url
self.new_page(url_to_open)
# Set up keyboard shortcuts
self._setup_shortcuts()
# Store Playwright objects
self.playwright = None
self.playwright_browser = None
self.browser_contexts = {}
self.dev_tools_windows = []
self.open_extension_popups = []
self.extension_actions = []
self.update_extension_buttons()
def closeEvent(self, event):
if self in Browser.instances:
Browser.instances.remove(self)
super().closeEvent(event)
def _get_themed_icon(self, standard_pixmap, color="white"):
"""Create a themed icon from a standard pixmap."""
icon = self.style().standardIcon(standard_pixmap)
pixmap = icon.pixmap(self.toolbar.iconSize())
painter = QPainter(pixmap)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
painter.fillRect(pixmap.rect(), QColor(color))
painter.end()
return QIcon(pixmap)
def _create_new_tab_button(self):
"""Create a button for adding new tabs."""
button = QPushButton("+")
button.setToolTip("New Tab")
button.clicked.connect(lambda: self.new_page(self.home_url))
return button
def _setup_shortcuts(self):
"""Set up keyboard shortcuts."""
# Ctrl+T: New Tab
new_tab_shortcut = QAction("New Tab", self)
new_tab_shortcut.setShortcut(QKeySequence("Ctrl+T"))
new_tab_shortcut.triggered.connect(lambda: self.new_page(self.home_url))
self.addAction(new_tab_shortcut)
# Ctrl+Tab: Cycle Tabs
cycle_tabs_shortcut = QAction("Cycle Tabs", self)
cycle_tabs_shortcut.setShortcut(QKeySequence("Ctrl+Tab"))
cycle_tabs_shortcut.triggered.connect(self.cycle_tabs)
self.addAction(cycle_tabs_shortcut)
# Ctrl+W: New Window
new_window_shortcut = QAction("New Window", self)
new_window_shortcut.setShortcut(QKeySequence("Ctrl+W"))
new_window_shortcut.triggered.connect(self.open_new_window)
self.addAction(new_window_shortcut)
# Ctrl+L: Focus URL bar
focus_url_shortcut = QAction("Focus URL", self)
focus_url_shortcut.setShortcut(QKeySequence("Ctrl+L"))
focus_url_shortcut.triggered.connect(self.url_input.setFocus)
self.addAction(focus_url_shortcut)
# F5: Reload page
reload_shortcut = QAction("Reload", self)
reload_shortcut.setShortcut(QKeySequence("F5"))
reload_shortcut.triggered.connect(self.reload_page)
self.addAction(reload_shortcut)
def cycle_tabs(self):
"""Cycle through the open tabs."""
if self.tabs.count() > 1:
next_index = (self.tabs.currentIndex() + 1) % self.tabs.count()
self.tabs.setCurrentIndex(next_index)
def on_tab_removed(self):
"""Close the window if the last tab is removed."""
if self.tabs.count() == 0:
self.close()
def handle_tab_detached(self, index, pos):
"""Handle a tab being detached to a new window."""
if index < 0 or index >= self.tabs.count():
return
widget = self.tabs.widget(index)
title = self.tabs.tabText(index)
try:
widget.urlChanged.disconnect(self.update_url)
widget.titleChanged.disconnect()
widget.loadStatusChanged.disconnect(self.update_status_bar)
except (TypeError, RuntimeError):
pass
# Create a new browser instance, which will take ownership of the widget.
new_browser = Browser(
debug=("--debug" in sys.argv),
detached_tab=(widget, title),
profile_path=self.profile_dir
)
new_browser.setGeometry(self.geometry())
new_browser.move(pos - QPoint(new_browser.width() // 4, 10))
new_browser.show()
# Now that the widget is safely reparented, remove the tab from the old browser.
# The on_tab_removed method will handle closing the window if it's empty.
self.tabs.removeTab(index)
self.on_tab_removed()
def new_page(self, url="about:blank"):
"""Create a new browser tab with the specified URL."""
# Create a new tab
tab = BrowserTab(parent=self.tabs, url=url, profile=self.profile)
# Store a reference to the Browser instance
tab.browser = self
tab.urlChanged.connect(self.update_url)
tab.titleChanged.connect(lambda title, tab=tab: self.update_title(title, tab))
tab.loadStatusChanged.connect(self.update_status_bar)
# Add the tab to the tab widget
index = self.tabs.addTab(tab, "New Tab")
self.tabs.setCurrentIndex(index)
return tab
def close_tab(self, index):
"""Close the tab at the specified index."""
if index < 0 or index >= self.tabs.count():
return
# Get the tab widget and schedule it for deletion.
tab = self.tabs.widget(index)
if tab:
tab.deleteLater()
# Remove the tab from the tab bar.
self.tabs.removeTab(index)
self.on_tab_removed()
def tab_changed(self, index):
"""Handle tab change events."""
if index < 0:
return
# Update the URL input
tab = self.tabs.widget(index)
if hasattr(tab, 'url'):
self.url_input.setText(tab.url)
self.update_status_bar()
def update_url(self, url):
"""Update the URL input when the page URL changes."""
self.url_input.setText(url)
# Update the tab's stored URL
current_tab = self.tabs.currentWidget()
if current_tab:
current_tab.url = url
def update_title(self, title, tab):
"""Update the tab title when the page title changes."""
index = self.tabs.indexOf(tab)
if index >= 0:
self.tabs.setTabText(index, title or "New Tab")
def navigate_to_url(self):
"""Navigate to the URL in the URL input."""
url = self.url_input.text().strip()
# Add http:// if no protocol is specified
if url and not url.startswith(("http://", "https://", "file://", "about:", "chrome://")):
url = "http://" + url
# Navigate the current tab
current_tab = self.tabs.currentWidget()
if current_tab:
current_tab.navigate(url)
def navigate_home(self):
"""Navigate to the home URL."""
current_tab = self.tabs.currentWidget()
if current_tab:
current_tab.navigate(self.home_url)
def navigate_to_chrome_url(self, url):
"""Navigate to a chrome:// URL."""
print(f"Navigating to chrome URL: {url}")
# Create a new tab specifically for chrome:// URLs
tab = self.new_page("about:blank")
# Ensure the tab is using our custom ChromeWebEnginePage
if hasattr(tab, 'web_view'):
# If the page isn't already a ChromeWebEnginePage, replace it
if not isinstance(tab.web_view.page(), ChromeWebEnginePage):
page = ChromeWebEnginePage(self.profile, tab.web_view)
tab.web_view.setPage(page)
# Now navigate to the chrome:// URL
tab.web_view.load(QUrl(url))
print(f"Created new tab for chrome URL: {url}")
def navigate_back(self):
"""Navigate back in the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.back()
def navigate_forward(self):
"""Navigate forward in the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.forward()
def reload_page(self):
"""Reload the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.reload()
def open_new_window(self):
"""Open a new browser window."""
new_browser = Browser(debug="--debug" in sys.argv, profile_path=self.profile_dir)
new_browser.show()
def toggle_devtools(self):
"""Opens the remote developer tools URL in a new window or shows extension debug info."""
# Create a dialog to show debug options
debug_dialog = QDialog(self)
debug_dialog.setWindowTitle("Developer Tools")
debug_dialog.resize(800, 600)
layout = QVBoxLayout()
# Add tabs for different debug options
tabs = QTabWidget()
# Tab 1: Remote DevTools
remote_tab = QWidget()
remote_layout = QVBoxLayout(remote_tab)
web_view = QWebEngineView()
web_view.load(QUrl("http://localhost:9222"))
remote_layout.addWidget(web_view)
# Tab 2: Extension Debug Info
ext_tab = QWidget()
ext_layout = QVBoxLayout(ext_tab)
# Add a text area to show preferences file content
prefs_label = QLabel("Preferences File Content:")
ext_layout.addWidget(prefs_label)
prefs_text = QTextEdit()
prefs_text.setReadOnly(True)
ext_layout.addWidget(prefs_text)
# Add a button to refresh the preferences content
refresh_button = QPushButton("Refresh Preferences")
ext_layout.addWidget(refresh_button)
# Add a button to force enable developer mode
force_button = QPushButton("Force Enable Developer Mode")
ext_layout.addWidget(force_button)
# Add a button specifically for fixing the Lovense extension
lovense_button = QPushButton("Fix Lovense Extension")
ext_layout.addWidget(lovense_button)
# Function to load and display preferences
def load_preferences():
prefs_file = os.path.join(self.profile_dir, "Default", "Preferences")
if os.path.exists(prefs_file):
try:
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
# Format the JSON for better readability
formatted_json = json.dumps(prefs_data, indent=2)
prefs_text.setText(formatted_json)
# Highlight extension developer mode status
ext_dev_mode = prefs_data.get('extensions', {}).get('developer_mode', False)
allow_file_access = prefs_data.get('extensions', {}).get('allow_file_access', False)
allow_user_scripts = prefs_data.get('extensions', {}).get('allow_user_scripts', False)
status_text = f"Extension Developer Mode: {ext_dev_mode}\n"
status_text += f"Allow File Access: {allow_file_access}\n"
status_text += f"Allow User Scripts: {allow_user_scripts}\n"
QMessageBox.information(debug_dialog, "Extension Settings Status", status_text)
except Exception as e:
prefs_text.setText(f"Error loading preferences: {e}")
else:
prefs_text.setText("Preferences file not found")
# Function to force enable developer mode
def force_developer_mode():
prefs_file = os.path.join(self.profile_dir, "Default", "Preferences")
if os.path.exists(prefs_file):
try:
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
# Ensure extensions section exists
if 'extensions' not in prefs_data:
prefs_data['extensions'] = {}
# Force enable all developer mode settings
prefs_data['extensions']['developer_mode'] = True
prefs_data['extensions']['allow_file_access'] = True
prefs_data['extensions']['allow_user_scripts'] = True
# Write back to file
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# Reload the profile
self.reload_profile_and_tabs()
QMessageBox.information(debug_dialog, "Success",
"Developer mode forcefully enabled. Profile reloaded.")
# Refresh the displayed preferences
load_preferences()
except Exception as e:
QMessageBox.critical(debug_dialog, "Error", f"Could not force developer mode: {e}")
else:
QMessageBox.warning(debug_dialog, "Error", "Preferences file not found")
# Function to specifically fix the Lovense extension
def fix_lovense_extension():
# Check if Lovense extension exists
lovense_dir = os.path.join(self.extensions_dir, "lovense")
if not os.path.exists(lovense_dir):
QMessageBox.warning(debug_dialog, "Error", "Lovense extension not found in extensions directory")
return
try:
# 1. Update preferences file with specific settings for Lovense
prefs_file = os.path.join(self.profile_dir, "Default", "Preferences")
if os.path.exists(prefs_file):
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
# Ensure extensions section exists
if 'extensions' not in prefs_data:
prefs_data['extensions'] = {}
# Force enable all developer mode settings
prefs_data['extensions']['developer_mode'] = True
prefs_data['extensions']['allow_file_access'] = True
prefs_data['extensions']['allow_user_scripts'] = True
# Add specific settings for Lovense extension
if 'settings' not in prefs_data['extensions']:
prefs_data['extensions']['settings'] = {}
# Get Lovense extension ID (directory name)
lovense_id = "lovense"
# Set specific permissions for Lovense
prefs_data['extensions']['settings'][lovense_id] = {
"active_permissions": {
"api": ["storage", "unlimitedStorage", "userScripts", "desktopCapture"],
"explicit_host": ["<all_urls>"],
"manifest_permissions": [],
"scriptable_host": ["<all_urls>"]
},
"granted_permissions": {
"api": ["storage", "unlimitedStorage", "userScripts", "desktopCapture"],
"explicit_host": ["<all_urls>"],
"manifest_permissions": [],
"scriptable_host": ["<all_urls>"]
},
"location": 1, # 1 = unpacked extension
"runtime_allowed_hosts": ["*://*.lovense.com/*", "*://localhost/*"],
"runtime_blocked_hosts": []
}
# Write back to file
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# 2. Create a secure_preferences file if it doesn't exist
secure_prefs_file = os.path.join(self.profile_dir, "Default", "Secure Preferences")
secure_prefs_data = {}
if os.path.exists(secure_prefs_file):
try:
with open(secure_prefs_file, 'r') as f:
secure_prefs_data = json.load(f)
except:
secure_prefs_data = {}
# Ensure extensions section exists
if 'extensions' not in secure_prefs_data:
secure_prefs_data['extensions'] = {}
# Add settings for Lovense
if 'settings' not in secure_prefs_data['extensions']:
secure_prefs_data['extensions']['settings'] = {}
# Set Lovense as trusted
secure_prefs_data['extensions']['settings']["lovense"] = {
"ack_external_install_prompt": True,
"installed_by_default": False,
"installed_by_oem": False,
"installed_by_policy": False,
"mark_acknowledged_external_install": True,
"was_installed_by_enterprise_policy": False
}
# Write secure preferences
with open(secure_prefs_file, 'w') as f:
json.dump(secure_prefs_data, f, indent=2)
# 3. Create Local State file with extension settings
local_state_file = os.path.join(self.profile_dir, "Local State")
local_state_data = {}
if os.path.exists(local_state_file):
try:
with open(local_state_file, 'r') as f:
local_state_data = json.load(f)
except:
local_state_data = {}
# Ensure extensions section exists
if 'extensions' not in local_state_data:
local_state_data['extensions'] = {}
# Enable developer mode in local state
local_state_data['extensions']['ui'] = {
"developer_mode": True
}
# Write local state
with open(local_state_file, 'w') as f:
json.dump(local_state_data, f, indent=2)
# Reload the profile
self.reload_profile_and_tabs()
QMessageBox.information(debug_dialog, "Success",
"Lovense extension has been fixed and profile reloaded.")
# Refresh the displayed preferences
load_preferences()
except Exception as e:
QMessageBox.critical(debug_dialog, "Error", f"Could not fix Lovense extension: {e}")
# Connect buttons to functions
refresh_button.clicked.connect(load_preferences)
force_button.clicked.connect(force_developer_mode)
lovense_button.clicked.connect(fix_lovense_extension)
# Load preferences initially
load_preferences()
# Add tabs to the tab widget
tabs.addTab(remote_tab, "Remote DevTools")
tabs.addTab(ext_tab, "Extension Debug")
layout.addWidget(tabs)
debug_dialog.setLayout(layout)
debug_dialog.show()
self.dev_tools_windows.append(debug_dialog)
debug_dialog.finished.connect(lambda: self.dev_tools_windows.remove(debug_dialog))
def update_status_bar(self):
"""Update the status bar based on the current tab's state."""
current_tab = self.tabs.currentWidget()
if not current_tab:
self.status_bar.showMessage("Ready")
return
if current_tab.is_loading:
self.status_bar.showMessage("Loading...")
else:
self.status_bar.showMessage("Ready")
def reload_profile_and_tabs(self):
"""Reloads the browser profile and recreates pages in all tabs."""
print("Reloading profile to apply extension changes...")
# 1. Save the URLs and current index of all open tabs
urls = []
current_index = self.tabs.currentIndex()
for i in range(self.tabs.count()):
tab = self.tabs.widget(i)
if hasattr(tab, 'web_view') and tab.web_view.url().isValid():
urls.append(tab.web_view.url().toString())
else:
urls.append("about:blank")
# 2. Store the old profile to delete it later
old_profile = self.profile
old_profile.clearHttpCache()
# 3. Clear all existing tabs. This removes references to pages using the old profile.
self.tabs.clear()
# 4. Create a new profile object. This forces a re-read from disk because
# the old one will be properly deleted.
self.profile = QWebEngineProfile(self.profile_dir, self)
# Re-enable extension developer mode and Autofill settings
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanPaste, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.WebGLEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled, True)
self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True)
# Set developer mode flag in preferences
self.profile.setHttpUserAgent(self.profile.httpUserAgent() + " ChromiumExtensionDevMode")
# Re-apply extension developer mode settings to preferences file
prefs_dir = os.path.join(self.profile_dir, "Default")
prefs_file = os.path.join(prefs_dir, "Preferences")
if os.path.exists(prefs_file):
try:
with open(prefs_file, 'r') as f:
prefs_data = json.load(f)
# Ensure extension developer mode is enabled
if 'extensions' not in prefs_data:
prefs_data['extensions'] = {}
prefs_data['extensions']['developer_mode'] = True
prefs_data['extensions']['allow_file_access'] = True
prefs_data['extensions']['allow_user_scripts'] = True
# Write the updated preferences back to the file
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# Check for Lovense extension and reconfigure it
lovense_dir = os.path.join(self.extensions_dir, "lovense")
if os.path.exists(lovense_dir):
print("Lovense extension found during reload, reapplying special configuration...")
# Update specific settings for Lovense extension in preferences
if 'settings' not in prefs_data['extensions']:
prefs_data['extensions']['settings'] = {}
# Set specific permissions for Lovense
prefs_data['extensions']['settings']["lovense"] = {
"active_permissions": {
"api": ["storage", "unlimitedStorage", "userScripts", "desktopCapture"],
"explicit_host": ["<all_urls>"],
"manifest_permissions": [],
"scriptable_host": ["<all_urls>"]
},
"granted_permissions": {
"api": ["storage", "unlimitedStorage", "userScripts", "desktopCapture"],
"explicit_host": ["<all_urls>"],
"manifest_permissions": [],
"scriptable_host": ["<all_urls>"]
},
"location": 1, # 1 = unpacked extension
"runtime_allowed_hosts": ["*://*.lovense.com/*", "*://localhost/*"],
"runtime_blocked_hosts": []
}
# Write updated preferences with Lovense settings
with open(prefs_file, 'w') as f:
json.dump(prefs_data, f, indent=2)
# Update secure preferences
secure_prefs_file = os.path.join(self.profile_dir, "Default", "Secure Preferences")
secure_prefs_data = {}
if os.path.exists(secure_prefs_file):
try:
with open(secure_prefs_file, 'r') as f:
secure_prefs_data = json.load(f)
except:
secure_prefs_data = {}
# Ensure extensions section exists
if 'extensions' not in secure_prefs_data:
secure_prefs_data['extensions'] = {}
# Add settings for Lovense
if 'settings' not in secure_prefs_data['extensions']:
secure_prefs_data['extensions']['settings'] = {}
# Set Lovense as trusted
secure_prefs_data['extensions']['settings']["lovense"] = {
"ack_external_install_prompt": True,
"installed_by_default": False,
"installed_by_oem": False,
"installed_by_policy": False,
"mark_acknowledged_external_install": True,
"was_installed_by_enterprise_policy": False
}
# Write secure preferences
with open(secure_prefs_file, 'w') as f:
json.dump(secure_prefs_data, f, indent=2)
# Update Local State file
local_state_file = os.path.join(self.profile_dir, "Local State")
local_state_data = {}
if os.path.exists(local_state_file):
try:
with open(local_state_file, 'r') as f:
local_state_data = json.load(f)
except:
local_state_data = {}
# Ensure extensions section exists
if 'extensions' not in local_state_data:
local_state_data['extensions'] = {}
# Enable developer mode in local state
local_state_data['extensions']['ui'] = {
"developer_mode": True
}
# Write local state
with open(local_state_file, 'w') as f:
json.dump(local_state_data, f, indent=2)
print("Lovense extension reconfiguration complete during reload")
except Exception as e:
print(f"Error updating preferences file during reload: {e}")
self.profile.installUrlSchemeHandler(b"qextension", self.extension_scheme_handler)
# 5. Re-create all tabs with the new profile
if not urls: # Ensure there's at least one tab
urls.append(self.home_url)
for url in urls:
self.new_page(url)
# 6. Restore the previously active tab
if current_index != -1 and current_index < len(urls):
self.tabs.setCurrentIndex(current_index)
# 7. Schedule the old profile for deletion. This is crucial.
old_profile.setParent(None)
old_profile.deleteLater()
print("Profile and tabs reloaded.")
self.update_extension_buttons()
def open_extension_popup_from_toolbar(self, ext_path):
"""Opens an extension's popup from a toolbar button click."""
ext_name = os.path.basename(ext_path)
manifest_path = os.path.join(ext_path, "manifest.json")
if not os.path.exists(manifest_path):
QMessageBox.warning(self, "Error", "manifest.json not found for this extension.")
return
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except Exception as e:
QMessageBox.critical(self, "Error", f"Could not read manifest.json: {e}")
return
popup_path = None
action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if action and action.get('default_popup'):
popup_path = action.get('default_popup')
if not popup_path:
# This should not happen if the button was created, but as a safeguard:
QMessageBox.information(self, "No Popup", "This extension does not have a popup.")
return
popup_file = os.path.join(ext_path, popup_path)
if not os.path.exists(popup_file):
QMessageBox.warning(self, "Error", f"Popup file not found: {popup_path}")
return
# Open the popup in a new non-modal dialog
popup_dialog = QDialog(self)
popup_dialog.setWindowTitle(manifest.get("name", "Extension Popup"))
layout = QVBoxLayout(popup_dialog)
web_view = QWebEngineView()
# The 'self' here is the Browser instance, so self.profile is correct
page = QWebEnginePage(self.profile, web_view)
web_view.setPage(page)
popup_url = QUrl(f"qextension://{ext_name}/{popup_path}")
web_view.load(popup_url)
layout.addWidget(web_view)
popup_dialog.setLayout(layout)
popup_dialog.resize(400, 600)
popup_dialog.show()
# Keep a reference to prevent garbage collection
self.open_extension_popups.append(popup_dialog)
popup_dialog.finished.connect(lambda: self.open_extension_popups.remove(popup_dialog))
def update_extension_buttons(self):
"""Scans for extensions and adds a button for each one with a popup to the main toolbar."""
# Remove previous extension buttons
for action in self.extension_actions:
self.toolbar.removeAction(action)
self.extension_actions.clear()
if not self.extensions_dir or not os.path.exists(self.extensions_dir):
return
# Find all enabled extension directories
for ext_name in sorted(os.listdir(self.extensions_dir)):
if ext_name.endswith(".disabled"):
continue
ext_path = os.path.join(self.extensions_dir, ext_name)
if not os.path.isdir(ext_path):
continue
manifest_path = os.path.join(ext_path, "manifest.json")
if not os.path.exists(manifest_path):
continue
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except Exception as e:
print(f"Error loading manifest for {ext_name}: {e}")
continue
# Check for a popup action
popup_path = None
action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if action and action.get('default_popup'):
popup_path = action.get('default_popup')
if popup_path:
# Create a button for this extension
button = QPushButton()
button.setFlat(True)
button.setIconSize(QSize(22, 22))
# Set the icon
icon_path = None
if manifest.get("icons"):
# Prefer a 24px or 16px icon for the toolbar
icon_path = manifest["icons"].get("24") or manifest["icons"].get("16") or next(iter(manifest["icons"].values()), None)
if icon_path:
full_icon_path = os.path.join(ext_path, icon_path)
if os.path.exists(full_icon_path):
button.setIcon(QIcon(full_icon_path))
button.setToolTip(manifest.get("name", ext_name))
button.clicked.connect(partial(self.open_extension_popup_from_toolbar, ext_path))
# Insert the button before the main "Extensions" button
action = self.toolbar.insertWidget(self.extensions_button_action, button)
self.extension_actions.append(action)
def debug_chrome_urls(self):
"""Debug chrome:// URLs by showing information and testing navigation."""
debug_dialog = QDialog(self)
debug_dialog.setWindowTitle("Chrome URL Debug")
debug_dialog.resize(600, 500)
layout = QVBoxLayout(debug_dialog)
# Add information about the current configuration
info_text = QTextEdit()
info_text.setReadOnly(True)
# Collect debug information
debug_info = [
"Chrome URL Debug Information:",
"----------------------------",
f"Profile Path: {self.profile_dir}",
f"User Agent: {self.profile.httpUserAgent()}",
"WebEngine Settings:",
f" LocalContentCanAccessRemoteUrls: {self.profile.settings().testAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls)}",
f" ErrorPageEnabled: {self.profile.settings().testAttribute(QWebEngineSettings.WebAttribute.ErrorPageEnabled)}",
f" FocusOnNavigationEnabled: {self.profile.settings().testAttribute(QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled)}",
f" AllowRunningInsecureContent: {self.profile.settings().testAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent)}",
"Environment Variables:",
f" QTWEBENGINE_CHROMIUM_FLAGS: {os.environ.get('QTWEBENGINE_CHROMIUM_FLAGS', 'Not set')}",
"----------------------------",
"Instructions:",
"1. Click 'Test Chrome URL' to open chrome://version/ in a new tab",
"2. If it doesn't work, try restarting the browser with --debug flag",
"3. Check if the custom ChromeWebEnginePage is being used correctly"
]
info_text.setText("\n".join(debug_info))
layout.addWidget(info_text)
# Add buttons for testing
button_layout = QHBoxLayout()
test_button = QPushButton("Test Chrome URL")
test_button.clicked.connect(lambda: self.navigate_to_chrome_url("chrome://version/"))
button_layout.addWidget(test_button)
close_button = QPushButton("Close")
close_button.clicked.connect(debug_dialog.accept)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
debug_dialog.exec()
def show_extensions(self):
"""Show the extensions dialog."""
dialog = ExtensionDialog(self, self.extensions_dir)
dialog.exec()
class QPlaywrightBrowser:
"""
A class that wraps the Playwright browser API and uses Qt6Browser internally.
This class exposes the Playwright API while using the Qt6 browser for display.
"""
def __init__(self):
self.app = QApplication.instance() or QApplication(sys.argv)
self.browser_ui = None
self.playwright = None
self.browser = None
self.contexts = {}
self.event_loop = None
async def launch(self, **kwargs):
"""Launch a new browser instance."""
# Initialize Playwright if not already done
if not self.playwright:
self.playwright = await async_playwright().start()
# Create the Qt browser UI
if not self.browser_ui:
self.browser_ui = Browser()
self.browser_ui.show()
# Launch the actual Playwright browser (hidden)
# We'll use this for API compatibility but display in Qt
self.browser = await self.playwright.chromium.launch(
headless=True, # Run headless since we're displaying in Qt
args = [
'--headless=new',
'--enable-features=Autofill',
]+os.environ.get('QTWEBENGINE_CHROMIUM_FLAGS').split(),
**kwargs
)
#
# Create a default context
default_context = await self.browser.new_context()
self.contexts["default"] = default_context
return self
async def new_context(self, **kwargs):
"""Create a new browser context."""
if not self.browser:
await self.launch()
# Create a new context in the Playwright browser
context_id = f"context_{len(self.contexts)}"
context = await self.browser.new_context(**kwargs)
self.contexts[context_id] = context
# Return a wrapper that provides both Playwright API and Qt UI
return QPlaywrightBrowserContext(self, context, context_id)
async def new_page(self, url="about:blank"):
"""Create a new page in the default context."""
if "default" not in self.contexts:
await self.launch()
# Create a new page in the Playwright browser
pw_page = await self.contexts["default"].new_page()
await pw_page.goto(url)
# Create a new tab in the Qt browser
qt_tab = self.browser_ui.new_page(url)
qt_tab.page = pw_page
# Return a wrapper that provides both Playwright API and Qt UI
return QPlaywrightPage(pw_page, qt_tab)
async def close(self):
"""Close the browser."""
# Close all Playwright contexts and browser
if self.browser:
for context_id, context in self.contexts.items():
await context.close()
await self.browser.close()
self.contexts = {}
self.browser = None
# Close the Qt browser UI
if self.browser_ui:
self.browser_ui.close()
self.browser_ui = None
# Close Playwright
if self.playwright:
await self.playwright.stop()
self.playwright = None
def run(self):
"""Run the application event loop."""
return self.app.exec()
def run_async(self, coro):
"""Run an async coroutine in the Qt event loop."""
if not self.event_loop:
self.event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.event_loop)
# Create a QTimer to process asyncio events
timer = QTimer()
timer.timeout.connect(lambda: self.event_loop.run_until_complete(asyncio.sleep(0)))
timer.start(10) # 10ms interval
# Run the coroutine
future = asyncio.run_coroutine_threadsafe(coro, self.event_loop)
return future
class QPlaywrightBrowserContext:
"""
A wrapper around a Playwright BrowserContext that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, qbrowser, context, context_id):
self.qbrowser = qbrowser
self.context = context
self.context_id = context_id
self.pages = []
async def new_page(self):
"""Create a new page in this context."""
# Create a new page in the Playwright context
pw_page = await self.context.new_page()
# Create a new tab in the Qt browser
qt_tab = self.qbrowser.browser_ui.new_page("about:blank")
qt_tab.page = pw_page
# Create a wrapper page
page = QPlaywrightPage(pw_page, qt_tab)
self.pages.append(page)
return page
async def close(self):
"""Close this context."""
# Close all pages
for page in self.pages:
await page.close()
# Close the Playwright context
await self.context.close()
# Remove from the qbrowser contexts
if self.context_id in self.qbrowser.contexts:
del self.qbrowser.contexts[self.context_id]
class QPlaywrightPage:
"""
A wrapper around a Playwright Page that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, pw_page, qt_tab):
self.pw_page = pw_page
self.qt_tab = qt_tab
# Forward Playwright page methods and properties
self.goto = pw_page.goto
self.click = pw_page.click
self.fill = pw_page.fill
self.type = pw_page.type
self.press = pw_page.press
self.wait_for_selector = pw_page.wait_for_selector
self.wait_for_navigation = pw_page.wait_for_navigation
self.wait_for_load_state = pw_page.wait_for_load_state
self.evaluate = pw_page.evaluate
self.screenshot = pw_page.screenshot
self.content = pw_page.content
self.title = pw_page.title
self.url = pw_page.url
async def close(self):
"""Close this page."""
# Close the Playwright page
await self.pw_page.close()
# Close the Qt tab
# We need to use the Qt event loop for this
index = self.qt_tab.parent.indexOf(self.qt_tab)
if index >= 0:
self.qt_tab.parent.close_tab(index)
async def main_async():
"""Async main function to run the browser."""
browser = QPlaywrightBrowser()
await browser.launch()
# Open a page
url = "https://www.google.com"
if len(sys.argv) > 1:
url = sys.argv[1]
await browser.new_page(url)
# Keep the browser running
while True:
await asyncio.sleep(0.1)
def main():
"""Main function to run the browser as a standalone application."""
# Check for extension developer mode flag
ext_dev_mode = "--ext-dev-mode" in sys.argv
if ext_dev_mode:
print("Extension developer mode explicitly enabled via command line")
# Enable Autofill features and extension developer mode via environment variables
chromium_flags = "--enable-features=AutofillEnableAccountWalletStorage,AutofillAddressProfileSavePrompt,AutofillCreditCardUpload,AutofillEnableToolbarStatusChip,AutofillKeyboardAccessory,AutofillShowAllSuggestionsOnPrefsCheckout,AutofillShowTypePredictions,AutofillUpstream,PasswordGeneration,PasswordGenerationBottomSheetUI,PasswordGenerationExperiment,ExtensionsToolbarMenu,ChromeUIDebugTools --extensions-on-chrome-urls --allow-file-access-from-files --allow-running-insecure-content --enable-user-scripts --allow-universal-access-from-files --disable-web-security --disable-site-isolation-trials --allow-insecure-localhost --ignore-certificate-errors --ignore-urlfetcher-cert-requests --disable-features=BlockInsecurePrivateNetworkRequests"
# Add additional flags for extension developer mode if explicitly requested
if ext_dev_mode:
chromium_flags += " --load-extension=assets/browser/extensions/lovense --disable-extensions-except=assets/browser/extensions/lovense --force-dev-mode-highlighting"
# Explicitly enable chrome:// URLs
chromium_flags += " --enable-chrome-urls --enable-features=ChromeUIDebugTools"
# Register chrome:// as a known scheme
chrome_scheme = QWebEngineUrlScheme(b"chrome")
chrome_scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme |
QWebEngineUrlScheme.Flag.LocalScheme |
QWebEngineUrlScheme.Flag.LocalAccessAllowed |
QWebEngineUrlScheme.Flag.ServiceWorkersAllowed)
QWebEngineUrlScheme.registerScheme(chrome_scheme)
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = chromium_flags
debug_mode = "--debug" in sys.argv
if debug_mode:
os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = "9222"
# Print environment variables for debugging
print(f"QTWEBENGINE_CHROMIUM_FLAGS: {os.environ.get('QTWEBENGINE_CHROMIUM_FLAGS')}")
if debug_mode:
print(f"QTWEBENGINE_REMOTE_DEBUGGING: {os.environ.get('QTWEBENGINE_REMOTE_DEBUGGING')}")
app = QApplication.instance() or QApplication(sys.argv)
# Allow Ctrl+C to kill the application gracefully
signal.signal(signal.SIGINT, signal.SIG_DFL)
app.setWindowIcon(QIcon('assets/logo.jpg'))
# Set a modern, dark theme
app.setStyle("Fusion")
dark_palette = app.palette()
dark_palette.setColor(dark_palette.ColorRole.Window, QColor(45, 45, 45))
dark_palette.setColor(dark_palette.ColorRole.WindowText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Base, QColor(25, 25, 25))
dark_palette.setColor(dark_palette.ColorRole.AlternateBase, QColor(53, 53, 53))
dark_palette.setColor(dark_palette.ColorRole.ToolTipBase, QColor(25, 25, 25))
dark_palette.setColor(dark_palette.ColorRole.ToolTipText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Text, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Button, QColor(53, 53, 53))
dark_palette.setColor(dark_palette.ColorRole.ButtonText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.BrightText, Qt.GlobalColor.red)
dark_palette.setColor(dark_palette.ColorRole.Link, QColor(42, 130, 218))
dark_palette.setColor(dark_palette.ColorRole.Highlight, QColor(42, 130, 218))
dark_palette.setColor(dark_palette.ColorRole.HighlightedText, Qt.GlobalColor.black)
app.setPalette(dark_palette)
# Set a better font
font = QFont("Cantarell", 10)
if platform.system() == "Windows":
font = QFont("Segoe UI", 10)
elif platform.system() == "Darwin":
font = QFont("San Francisco", 10)
app.setFont(font)
# Create a dummy parent for dialogs to ensure they are properly modal
dummy_parent = QWidget()
# Get profile path
profile_path = ProfileDialog.get_profile_path(dummy_parent)
if not profile_path:
return 0 # Exit gracefully
# Create a simple browser directly without Playwright for now
# This ensures we at least get a window showing
initial_url = None
if len(sys.argv) > 1 and not sys.argv[1].startswith('--'):
initial_url = sys.argv[1]
browser = Browser(initial_url=initial_url, debug=debug_mode, profile_path=profile_path)
browser.show()
print("Browser window should be visible now")
# Run the Qt event loop
return app.exec()
if __name__ == "__main__":
sys.exit(main())
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QLineEdit, QToolBar, QPushButton, QVBoxLayout, QWidget
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineProfile, QWebEngineScript
from PySide6.QtCore import QUrl, Qt, QObject, Slot, QFile, QIODevice
from PySide6.QtWebChannel import QWebChannel
import json
import os
import pkg_resources
class ChromeRuntimeBridge(QObject):
"""Emulates a subset of chrome.runtime API for communication."""
def __init__(self, parent=None):
super().__init__(parent)
self.listeners = {}
@Slot(str, result=str)
def sendMessage(self, message):
"""Simulate chrome.runtime.sendMessage."""
print(f"Received message from webpage: {message}")
response = {"status": "Message received", "data": message}
return json.dumps(response)
@Slot(str)
def addListener(self, callback_id):
"""Simulate chrome.runtime.onMessage.addListener."""
self.listeners[callback_id] = True
print(f"Listener added with ID: {callback_id}")
class WebEnginePage(QWebEnginePage):
"""Custom QWebEnginePage to inject chrome.runtime emulation."""
def __init__(self, profile, parent=None):
super().__init__(profile, parent)
self.runtime_bridge = ChromeRuntimeBridge(self)
self.channel = QWebChannel(self)
self.setWebChannel(self.channel)
self.channel.registerObject("runtimeBridge", self.runtime_bridge)
# Inject QWebChannel.js and chrome.runtime emulation
self.inject_qwebchannel()
self.inject_chrome_runtime()
def inject_qwebchannel(self):
"""Inject qwebchannel.js from Qt installation."""
# User-configurable path to qwebchannel.js (update this if needed)
#custom_qwebchannel_path = None # e.g., "/path/to/qwebchannel.js"
custom_qwebchannel_path = "/home/nextime/shmcamstudio/assets/browser/js/qwebchannel.js"
# Try to find qwebchannel.js
possible_paths = [
custom_qwebchannel_path,
pkg_resources.resource_filename('PySide6', 'Qt/libexec/qwebchannel.js') if custom_qwebchannel_path is None else None,
"/usr/lib64/qt6/libexec/qwebchannel.js", # Linux fallback
"/usr/lib/qt6/libexec/qwebchannel.js", # Alternative Linux path
"/usr/local/Cellar/qt/6.x.x/libexec/qwebchannel.js" # macOS Homebrew
]
js_file_path = None
for path in [p for p in possible_paths if p]:
if os.path.exists(path):
js_file_path = path
break
if js_file_path:
print(f"Found qwebchannel.js at: {js_file_path}")
js_file = QFile(js_file_path)
if js_file.open(QIODevice.ReadOnly):
script_content = str(js_file.readAll(), 'utf-8')
js_file.close()
web_script = QWebEngineScript()
web_script.setSourceCode(script_content)
web_script.setName("qwebchannel")
web_script.setWorldId(QWebEngineScript.MainWorld)
web_script.setInjectionPoint(QWebEngineScript.DocumentCreation)
web_script.setRunsOnSubFrames(True)
self.scripts().insert(web_script)
else:
print(f"Warning: Failed to open qwebchannel.js at {js_file_path}")
else:
print("Warning: qwebchannel.js not found in any known paths. WebChannel may not work.")
def inject_chrome_runtime(self):
"""Inject JavaScript to emulate chrome.runtime API."""
js_content = """
(function() {
console.log('chrome_runtime.js injected');
// Wait for QWebChannel and runtimeBridge to be ready
function initChromeRuntime() {
if (window.qt && window.qt.webChannelTransport && typeof QWebChannel !== 'undefined') {
new QWebChannel(qt.webChannelTransport, function(channel) {
window.runtimeBridge = channel.objects.runtimeBridge;
window.chrome = window.chrome || {};
window.chrome.runtime = {
sendMessage: function(message, callback) {
if (window.runtimeBridge) {
window.runtimeBridge.sendMessage(JSON.stringify(message)).then(function(response) {
if (callback) {
callback(JSON.parse(response));
}
});
} else {
console.error('runtimeBridge not available');
}
},
onMessage: {
addListener: function(callback) {
const callbackId = 'listener_' + Math.random().toString(36).substr(2, 9);
if (window.runtimeBridge) {
window.runtimeBridge.addListener(callbackId);
window[callbackId] = callback;
} else {
console.error('runtimeBridge not available');
}
}
}
};
window.chromeRuntimeReady = true;
console.log('chrome.runtime initialized');
});
} else {
console.log('Waiting for QWebChannel...');
setTimeout(initChromeRuntime, 50);
}
}
initChromeRuntime();
})();
"""
# Write JS content to a file
js_file_path = "chrome_runtime.js"
with open(js_file_path, "w") as f:
f.write(js_content)
# Read and inject the JS file
js_file = QFile(js_file_path)
if js_file.open(QIODevice.ReadOnly):
script_content = str(js_file.readAll(), 'utf-8')
js_file.close()
web_script = QWebEngineScript()
web_script.setSourceCode(script_content)
web_script.setName("chromeRuntimeEmulation")
web_script.setWorldId(QWebEngineScript.MainWorld)
web_script.setInjectionPoint(QWebEngineScript.DocumentReady)
web_script.setRunsOnSubFrames(True)
self.scripts().insert(web_script)
# Clean up the JS file
if os.path.exists(js_file_path):
os.remove(js_file_path)
class BrowserWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Qt6 Web Browser with chrome.runtime Emulation")
self.resize(1000, 600)
# Create WebEngineView
self.profile = QWebEngineProfile.defaultProfile()
self.web_view = QWebEngineView()
self.web_page = WebEnginePage(self.profile, self.web_view)
self.web_view.setPage(self.web_page)
# Create URL bar
self.url_bar = QLineEdit()
self.url_bar.setPlaceholderText("Enter URL and press Enter")
self.url_bar.returnPressed.connect(self.load_url)
# Create navigation toolbar
toolbar = QToolBar()
self.addToolBar(Qt.TopToolBarArea, toolbar)
back_btn = QPushButton("Back")
back_btn.clicked.connect(self.web_view.back)
toolbar.addWidget(back_btn)
forward_btn = QPushButton("Forward")
forward_btn.clicked.connect(self.web_view.forward)
toolbar.addWidget(forward_btn)
reload_btn = QPushButton("Reload")
reload_btn.clicked.connect(self.web_view.reload)
toolbar.addWidget(reload_btn)
toolbar.addWidget(self.url_bar)
# Set up main layout
central_widget = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.web_view)
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
# Connect URL change signal
self.web_view.urlChanged.connect(self.update_url_bar)
# Load a default page with test script
self.load_test_page()
def load_url(self):
url = self.url_bar.text()
if not url.startswith("http"):
url = "https://" + url
self.web_view.setUrl(QUrl(url))
def update_url_bar(self, qurl):
self.url_bar.setText(qurl.toString())
def load_test_page(self):
"""Load a test page with chrome.runtime API usage."""
html = """
<!DOCTYPE html>
<html>
<head>
<title>Test chrome.runtime</title>
</head>
<body>
<h1>Test chrome.runtime API</h1>
<button onclick="testSendMessage()">Send Message</button>
<p id="response">Response will appear here</p>
<script>
function initChromeRuntimeTests() {
if (window.chrome && window.chrome.runtime && window.chrome.runtime.onMessage && window.runtimeBridge) {
console.log('chrome.runtime tests initialized');
// Test chrome.runtime.sendMessage
window.testSendMessage = function() {
chrome.runtime.sendMessage({ greeting: "Hello from webpage!" }, function(response) {
document.getElementById('response').innerText =
'Response: ' + JSON.stringify(response);
});
};
// Test chrome.runtime.onMessage.addListener
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
console.log('Received message:', message);
sendResponse({ received: true });
});
} else {
console.log('chrome.runtime or runtimeBridge not ready, retrying...');
setTimeout(initChromeRuntimeTests, 50);
}
}
// Wait for chrome.runtime to be ready
function waitForChromeRuntime() {
if (window.chromeRuntimeReady) {
initChromeRuntimeTests();
} else {
console.log('Waiting for chromeRuntimeReady...');
setTimeout(waitForChromeRuntime, 50);
}
}
waitForChromeRuntime();
</script>
</body>
</html>
"""
self.web_view.setHtml(html, QUrl("http://localhost"))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = BrowserWindow()
window.show()
sys.exit(app.exec())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_extension.py - A script to test the chrome.runtime API emulation with a test extension
This script launches the browser with the test extension loaded and opens the test extension's popup
to verify that the chrome.runtime API emulation works correctly.
"""
import os
import sys
import time
from test import main as browser_main
def main():
"""Main function to run the browser with the test extension."""
# Make sure the test extension exists
test_ext_dir = "assets/browser/extensions/test-extension"
if not os.path.exists(test_ext_dir):
print(f"Error: Test extension directory not found: {test_ext_dir}")
return 1
# Check for manifest.json
manifest_path = os.path.join(test_ext_dir, "manifest.json")
if not os.path.exists(manifest_path):
print(f"Error: Manifest file not found: {manifest_path}")
return 1
# Set up command line arguments
sys.argv = [sys.argv[0], "--ext-dev-mode"]
# Launch the browser
print("Launching browser with test extension...")
return browser_main()
if __name__ == "__main__":
sys.exit(main())
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment