Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

A little lost

I rarely post asking for help with code-related things and am only giving up and posting because I have been doing this for several hours, getting nowhere, and have to go to work. I promise you I tried searching for answers but am just not getting my brain to wrap around this.  Ultimately, I would like to just learn how to get a google chrome extension to talk to roll20. In the simplest form, I really want to see how pressing a button within the extension could trigger sendChat and post a very simple message (such as "Hello world"). I think if I can see how that's done, I'll be able to figure it out from there.  Where I'm stuck: sendChat calls a function as an arg (and I read that is a predicated function -- but I truly am lost on what that is or how to interact with it).  on(sendChat...) also seems to be an event listener??? But, I want to trigger the event via the button. I know DND Beyond does this... But, how? Even if you can point me in the right direction or correct my moronity (neologism), I'd be grateful. 
1664466784
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
If you are creating an extension, the API docs are no use to you. Those docs are for the sandboxed internal API. It has no access to external resources, and I don't think it's even exposed externally. There is no documentation for what you're trying to do, so you'll need to dig into the front end code of a game and just start tracing what is triggered by what and what functions are exposed that you need to call.
1664468914
timmaugh
Pro
API Scripter
Oosh discussed a similar setup, connecting a dummy player to an external extension, here . Another approach to investigate might be triggering the actual event listeners of the page. The on() listeners are for Roll20's custom event dispatching, and aren't really 'gettable' things if you're listening for an event. (Think of them more as an array of functions to which Roll20 issues arguments based on game conditions... stepping through the array until all "listeners" have been notified... rather than relying on the native event registration of a page.) From an extension, you'd have full access to the DOM, but you'd have to (I think) inject to the chat input, then trigger the event listener attached to the "Send" button. I don't think your js-issued event will be trusted, so like Scott said, you'll have to hunt in the Developer Console to look at the listeners attached and what they call, and then try to call those functions directly. Good luck! If you get this particular nut cracked, post back so others can poach ... um, that is, benefit from what you learn!
Adam F. said: ... I know DND Beyond does this... But, how? I assume you mean the third-party Beyond20 extension? Why not check out the source code for for it? It's on Github.
Hey guys, I know no one had an exact answer, but this REALLY helps the search. I had actually pulled the source code for Beyond20 (despite my getting the name wrong). I was a bit over inundated, but I think it's becoming clearer I just need to wade into that code and see if I can figure this out from there. I was hoping someone had a quick answer, but it looks like it's just a more complicated problem than I originally conceptualized (and that's okay). I'll dig in (and report back if I find an answer).  Thank you for the quick and generous responses. 
1664497530

Edited 1664497740
Oosh
Sheet Author
API Scripter
There are definitely more sophisticated ways to approach it, but here's a simple one that hooks straight into the DOM as Tim alluded to. You can run it straight from the console as an example: const postToChat = (msg) => { const chatInputElement = document.querySelector('#textchat-input textarea'), chatButtonElement = document.querySelector('#textchat-input .btn'); if (chatInputElement && chatButtonElement) { const activeText = chatInputElement.value; chatInputElement.value = msg; chatButtonElement.click(); if (activeText) setTimeout(() => chatInputElement.value = activeText, 10); } } It's crude, but it's a starting point. It also puts back any text already in the textarea when its done, in case you were typing when the function fired. You could combine this with a Mod script to pass it through to sendChat() to expand your posting options to include HTML. But you're right, it's going to be more complex than you thought - you'll likely need to understand content script injection & background script context to get everything working smoothly.
1664552677

Edited 1664555797
So I think this makes a lot of conceptual sense and I'm getting a better hang for this already. But, I'm a bit stuck now on the query selectors because those don't seem to be working.  For what it's worth, I also attempted  const foo = document.querySelector('#textchat-input'); alert(foo); Which also returns null. Digging into the Beyond20 code, they use: function postChatMessage(message, character = null) { let set_speakingas = true; const old_as = speakingas.value; if (character) { character = character.toLowerCase().trim(); for (let i = 0; i < (speakingas.children.length); i++) { if (speakingas.children[i].text.toLowerCase().trim() === character) { speakingas.children[i].selected = true; set_speakingas = false; break; } } } if (set_speakingas) speakingas.children[0].selected = true; const old_text = txt.value; txt.value = message; btn.click(); txt.value = old_text; speakingas.value = old_as; } But again, "chat" is null. I don't get it. 
1664557366
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Where are you running your code Adam?
Uploaded to google chrome as an extension. 
I think this is the issue, but I need to work through how to solve it. Content Security Policy blocks inline execution of scripts and stylesheets The Content Security Policy (CSP) prevents cross-site scripting attacks by blocking inline execution of scripts and style sheets. To solve this, move all inline scripts (e.g.  onclick=[JS code] ) and styles into external files. ⚠️ Allowing inline execution comes at the risk of script injection via injection of HTML script elements. If you absolutely must, you can allow inline script and styles by: adding  unsafe-inline  as a source to the CSP header adding the hash or nonce of the inline script to your CSP header. AFFECTED RESOURCES 1 directive Directive Element Source Location Status script-src-elem app.roll20.net/:6 blocked Learn more: Content Security Policy - Inline Code
1664568881

Edited 1664569095
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Right, but what piece of the extension are you running your code in? Your background script? A content script? or are you injecting a script element into the Roll20 page with the code you want to run? Just processed what your last post means. So you're injecting a script, but you're injecting it as <script type="text/javascript"> // Your code here </script> You need to inject it as a script tag referencing your a js file containing your code: <script src="chrome-extension://..."></script> You can read up on how to get that url for your script file on the google extension docs. I also make extensive use of it in my Autocode extension .
I think this is a red herring. I read more about inline-code and it's not what's going on here. I don't have any inline-code.  Right now, the full and complete literal code in the chrome extension is entirely this: index.html <div id="test"> <button id="myButton">Press me</button> </div> <script src="libs/jquery-3.4.1.min.js" charset="UTF-8"></script> <script src="script.js"></script> script.js document.querySelector("button#myButton").addEventListener("click", postToChat); function postToChat(){ const message = "testing"; const chatInputElement = document.querySelector('#textchat-input textarea'); const chatButtonElement = document.querySelector('#textchat-input .btn'); if(chatInputElement==null){alert("chatinputnull");} if(chatButtonElement==null){alert("chatButtonElementnull");} } manifest.json { "name": "Charms Check Roll20 Extension", "version": "0.0.1", "description": "A pre-alpha attempt to create a roll20 plugin for Charms Check", "manifest_version": 1, "author": "Mr Liioadin", "action":{ "default_popup": "index.html", "default_title": "Charms Check Character Roller" } } It feels to me almost as if this is all running on a different window and not on the active chrome tag. That's my latest hypothesis anyway. 
1664634218
Oosh
Sheet Author
API Scripter
So... I've never looked at 'default_popup', and not really sure what that does exactly. But it looks like your extension is launching a web page fragment, which has an embedded script. These are not in the context of the Roll20 tab, so you can't access anything in the Roll20 DOM. Context & scope are important in extension-land. Poking a bit further into Scott's work, here is where he injects a content script into the web page. That's how you get access to the DOM in the Roll20 tab - the background script you launch directly from your extension won't have access to any of the websites open in the browser until you do something like this. The background script that serves as the entry point for an extension has access to some API's that web pages can't access, for some fairly serious security reasons. That does mean we have to jump through some hoops to get direct access to the DOM scope of any open web page. A necessary evil to make sure random web pages can't drive the other way up the track, and hijack your browser extensions... that would be extremely nasty, due to the lower level API's the extension has access to. If you look at what you posted above - you've embedded your script in index.html... so, when you try to grab an element with document.querySelector, which document will it look in? That's right - index.html. That isn't where any of the Roll20 innards lie, that's a tiny page fragment you just wrote. TLDR: extensions are tricky
Thanks. That helps me confirm it's an issue of scope. And, this code provides a great starting point to figure out the best way to hook in. I'll be back with more updates. Oosh, thanks so much for your help. 
1664637428
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Extensions essentially have 4 sandboxes Background.js: has no html access to anything. Essentially acts like a server Popup: has access to the pop-up html. Can sort of access the actual webpage through some trickery Content scripts: can get and manipulate the html of the main page, but does not have access to other J's defined variables on the page. Injected scripts: these can be put into the page as new script elements by content scripts. These work just like normal script tags that reference a J's file, but the src url must be constructed by the chrome url helper functions
So, I do get that this is complicated, but it's not that &nbsp;complicated. The concepts related to injection, security, scope are all perfectly understandable. I just struggle for syntax and language since I'm not trained in this. I think a lot of getting us newbies to understand is helping us know the language, what to focus on, and what to type (syntax). The conceptual stuff really isn't too bad, I don't think (we'll see if I still feel that way when I get into multithreading or something). Regardless, here's the solution: manifest.json { "name": "Charms Check Roll20 Extension", "version": "0.0.1", "description": "A pre-alpha attempt to create a roll20 plugin for Charms Check", "content_scripts": [ { "matches": ["<a href="https://app.roll20.net/editor/" rel="nofollow">https://app.roll20.net/editor/</a>"], "js": ["script.js"], "run_at": "document_end" } ], "manifest_version": 3, "author": "MrLiioadin" } I figured out to make these changes because you mentioned that you haven't really seen "default_popup" very much. That tells me I probably should look for a different solution and this video showed me one.&nbsp; script.js window.setTimeout(test, 5000); //ensures page is fully loaded before executing functions function test (){ postToChat("Charms Check Extension is ready"); } const postToChat = (msg) =&gt; { const chatInputElement = document.querySelector('#textchat-input textarea'), chatButtonElement = document.querySelector('#textchat-input .btn'); if (chatInputElement &amp;&amp; chatButtonElement) { const activeText = chatInputElement.value; chatInputElement.value = msg; chatButtonElement.click(); if (activeText) setTimeout(() =&gt; chatInputElement.value = activeText, 10); } } This now runs on the page directly because manifest.json is telling it to (which was the key element I was missing before).&nbsp;
BACK WITH A HUGE SUCCESS! I have a proof-of-concept version working. My code really needs to be cleaned up, functions renamed, and certain others streamlined. And, I still want to explore some other functionality. But here's the rough structure.&nbsp; I have a wordpress page with a pro subscription to allow me to use plugs. I'm using the scripts n styles plugin to allow me to directly inject javascript onto the page I'm using the formidable forms plugin to organize my database and to display the character sheet and its data. The wordpress javascript is: function randbetween(min, max) { // min and max included var val = Math.floor(Math.random() * (max - min + 1) + min); return val; } function roll(rolltype){ var abilityval = getAbility(rolltype); var skillval = getSkill(rolltype); var bonusval = 0; var penaltyval = 0; //var x = ability+skill+bonus+penalty+randbetween(1,10); console.log("rolltype: " + rolltype); console.log("ability val: " + abilityval); console.log("ability val: " + skillval); var randval = randbetween(1,10); var rollval = abilityval + skillval + bonusval + penaltyval + randval; console.log("Rand Val: " + randval); var msg = "Final Roll Value: " + rollval; alert("Final Roll Value: " + rollval); window.parent.postMessage(msg,"*"); } function getAbility(rolltype){ switch(rolltype){ case "Finesse": return parseInt(document.getElementById("Finesse").innerHTML); case "Intelligence": return parseInt(document.getElementById("Intelligence").innerHTML); case "Spirit": return parseInt(document.getElementById("Spirit").innerHTML); case "Power": return parseInt(document.getElementById("Power").innerHTML); case "Charms": return parseInt(document.getElementById("Power").innerHTML); case "Transfiguration": return parseInt(document.getElementById("Power").innerHTML); case "Defense": return parseInt(document.getElementById("Power").innerHTML); case "DarkArts": return parseInt(document.getElementById("Power").innerHTML); case "Arithmancy": return parseInt(document.getElementById("Intelligence").innerHTML); case "History": return parseInt(document.getElementById("Intelligence").innerHTML); case "Muggles": return parseInt(document.getElementById("Intelligence").innerHTML); case "Runes": return parseInt(document.getElementById("Intelligence").innerHTML); case "Herbology": return parseInt(document.getElementById("Finesse").innerHTML); case "Flying": return parseInt(document.getElementById("Finesse").innerHTML); case "Artificing": return parseInt(document.getElementById("Finesse").innerHTML); case "Potions": return parseInt(document.getElementById("Finesse").innerHTML); case "Alchemy": return parseInt(document.getElementById("Finesse").innerHTML); case "SocialSkills": return parseInt(document.getElementById("Spirit").innerHTML); case "Perception": return parseInt(document.getElementById("Spirit").innerHTML); case "Creatures": return parseInt(document.getElementById("Spirit").innerHTML); case "Divination": return parseInt(document.getElementById("Spirit").innerHTML); case "Astronomy": return parseInt(document.getElementById("Spirit").innerHTML); default: return 0; } } function getSkill(rolltype){ switch(rolltype){ case "Charms": return parseInt(document.getElementById("Charms").innerHTML); case "Transfiguration": return parseInt(document.getElementById("Transfiguration").innerHTML); case "Defense": return parseInt(document.getElementById("Defense").innerHTML); case "DarkArts": return parseInt(document.getElementById("DarkArts").innerHTML); case "Arithmancy": return parseInt(document.getElementById("Arithmancy").innerHTML); case "History": return parseInt(document.getElementById("History").innerHTML); case "Muggles": return parseInt(document.getElementById("Muggles").innerHTML); case "Runes": return parseInt(document.getElementById("Runes").innerHTML); case "Herbology": return parseInt(document.getElementById("Herbology").innerHTML); case "Flying": return parseInt(document.getElementById("Flying").innerHTML); case "Artificing": return parseInt(document.getElementById("Artificing").innerHTML); case "Potions": return parseInt(document.getElementById("Potions").innerHTML); case "Alchemy": return parseInt(document.getElementById("Alchemy").innerHTML); case "SocialSkills": return parseInt(document.getElementById("SocialSkills").innerHTML); case "Perception": return parseInt(document.getElementById("Perception").innerHTML); case "Creatures": return parseInt(document.getElementById("Creatures").innerHTML); case "Divination": return parseInt(document.getElementById("Divination").innerHTML); case "Astronomy": return parseInt(document.getElementById("Astronomy").innerHTML); default: return 0; } } function getBonus(){ //should include equipment } function getPenalty(){ } function parseLinesToArray() { var elements = document.getElementById('traitlist').innerHTML; var split = elements.split(', '); for (var i =0; i&lt;split.length; i++) { console.log(split[i].toString()); } } On the browser extension I'm using the following files: manifest.json { "name": "Charms Check Roll20 Extension", "version": "0.0.1", "description": "A pre-alpha attempt to create a roll20 plugin for Charms Check", "content_scripts": [ { "css": ["fullscreen.css"], "matches": ["<a href="https://app.roll20.net/editor/" rel="nofollow">https://app.roll20.net/editor/</a>"], "html": ["index.html"], "js": ["script.js"], "run_at": "document_end" } ], "manifest_version": 3, "author": "MrLiioadin", "action":{ "default_popup": "index.html", "default_title": "Charms Check Character Roller" } } script.js window.setTimeout(test, 5000); //ensures page is fully loaded before executing functions function test (){ addiframe(); getElement(); postToChat("Charms Check Extension is ready"); } const addiframe = function(){ const myiframe = document.createElement('iframe'); myiframe.setAttribute("id", "dynamiciframe"); myiframe.setAttribute("class", "ui-dialog ui-widget ui-widget-content ui-corner-all initiativedialog ui-draggable ui-resizable ui-dialog-buttons"); myiframe.setAttribute("src","<a href="https://charmscheck.com/character-sheet/entry/865/" rel="nofollow">https://charmscheck.com/character-sheet/entry/865/</a>"); document.body.appendChild(myiframe); postToChat("iframecreated"); } const postToChat = (msg) =&gt; { const chatInputElement = document.querySelector('#textchat-input textarea'), chatButtonElement = document.querySelector('#textchat-input .btn'); if (chatInputElement &amp;&amp; chatButtonElement) { const activeText = chatInputElement.value; chatInputElement.value = msg; chatButtonElement.click(); if (activeText) setTimeout(() =&gt; chatInputElement.value = activeText, 10); } } function getElement(){ var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; var eventer = window[eventMethod]; var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message"; // Listen to message from child window eventer(messageEvent,function(e) { var key = e.message ? "message" : "data"; var data = e[key]; postToChat(data); },false); } fullscreen.css (misnomer, it's not a fullscreen script) #dynamiciframe{ overflow:hidden; overflow-x:hidden; overflow-y:hidden; height:30%; width:15%; position:absolute; left:60px; top:5px; border-style: solid; border-width: 3px; position:absolute; z-index:1; } Explanation: On the wordpress side, each button has its own id and I'm using the id to grab the innerHTML to get the value held for that particular attribute. When the button is pressed, all rolling is handled on the wordpress side to allow me greater control such as eventually having specialized rolls and allowing me to build out a compendium in formidable forms. The iframe lives inside the roll20 body.&nbsp; The result of the roll is passed to the iframe's parent window (the roll20 body). The result is posted to an alert (I know, it's annoying. I'll remove it later) and then run through Oosh's code to postToChat. And, scene.