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

Reading text from GMNotes via API

December 11 (7 years ago)

Edited December 11 (7 years ago)
First, thanks for the help on the previous issue. I've gotten the following api to work...sort of
on('ready',() => {
    on('chat:message',(msg) => {
        if('api'===msg.type && /^!mapinfo/i.test(msg.content) && playerIsGM(msg.playerid)){
            _.chain(msg.selected)
                .map((o)=>getObj('graphic',o._id))
                .reject(_.isUndefined)
                .each((t)=>{
                    sendChat("Map Info", `/w gm &{template:desc} {{desc=${t.get('gmnotes')}}}`
                    );
                });
        }
    });
});
However, as you can see I'm reading the gmnotes from the selected token. These are tokens I have on the GM layer that contain information about the encounters. When I print them to chat however, everything is being converted to html characters, as in the output looks like this:

%3Ch3%3E11.%20Elevator%2C%20Upper%20Level%3C/h3%3EA%20ring-shaped%20gantry%20is%20bolted%20to%20the%20wall%20of%20the%20elevator%20shaft%2C%2050%20feet%20above%20the%20floor%20of%20the%20lower%20level%20%28area%2024%29.%20When%20the%20elevator%20stops%20here%2C%20the%20platform%20is%20level%20with%20the%20gantry.%20The%20circular%20space%20that%20the%20gantry%20surrounds%20is%20just%20wide%20enough%20for%20the%20platform%20to%20pass%20through%20it.%3Cbr%3E%3Cbr%3ENo%20guards%20are%20stationed%20here.%20Characters%20can%20hear%20the%20rattling%20of%20chains%20all%20around%20them.

Is there a way to get it to keep the spaces and carriage returns?
December 11 (7 years ago)

Edited December 11 (7 years ago)
Jakob
Sheet Author
API Scripter
Use decodeURIComponent(t.get('gmnotes')) instead.

Better, use The Aaron's solution, I forgot that you need special handling for line breaks.
December 11 (7 years ago)
The Aaron
Pro
API Scripter
Here's a Snippet I made that does that:
on('ready',function(){
    'use strict';

    on('chat:message',function(msg){
        if('api' === msg.type && msg.content.match(/^!gmnote/) && playerIsGM(msg.playerid) ){
            let match=msg.content.match(/^!gmnote-(.*)$/),
                regex;
            if(match && match[1]){
                regex = new RegExp(`^${match[1]}`,'i');
            }
                                
            _.chain(msg.selected)
                .map( s => getObj('graphic',s._id))
                .reject(_.isUndefined)
                .reject((o)=>o.get('gmnotes').length===0)
                .each( o => {
                    if(regex){
                        let lines=_.filter(decodeURIComponent(o.get('gmnotes')).split(/(?:[\n\r]+|<br\/?>)/),(l)=>regex.test(l)).join('\r');
                        sendChat(o.get('name'),lines);
                    } else {
                        sendChat(o.get('name'),decodeURIComponent(o.get('gmnotes')));
                    }
                });
        }
    });
});



December 11 (7 years ago)
Actually, I think for simplicity, I'm just going to create blank NPCs and link them to the tokens. That way, all I have to do is shift + double click on them to bring up the bio page.
December 11 (7 years ago)
But I'll try this as well! I didn't see it appear until after I tried a few other things.
December 11 (7 years ago)
That works really well by the way!
December 11 (7 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
The advantage to using the Aaron's script is that it reads from the token's GM notes, meaning you only have to have one Notes character. Every token and its notes is unique.

BTW Aaron, I modified your script to output into a role template for formatting. It crashes the API every time it encounters certain characters, such as a quote mark. I generally have to clean up text to prevent that, rather than paste from a word processor. Is this likely from what I have done, or does the vanilla script do that as well?


on('ready',function(){
'use strict';
on('chat:message',function(msg){
if('api' === msg.type && msg.content.match(/^!gmnote/) && playerIsGM(msg.playerid) ){
_.chain(msg.selected)
.map( s => getObj('graphic',s._id))
.reject(_.isUndefined)
.each( o => {
sendChat(o.get('name'),'/w gm &{template:5e-shaped}{{title=' + decodeURIComponent(o.get('name')) +'}} {{text='+ decodeURIComponent(o.get('gmnotes'))+'}}');
});
}
});
});
December 11 (7 years ago)
The Aaron
Pro
API Scripter
Hmm.  You mean whenever there are quotes in the notes field? Probably it could happen on mine as well.  decodeURIComponent can throw exceptions but I don't know what would cause that to happen.  If you have an example of a token where it happens, I'd be happy to come figure out the precise issue. 

BTW, You don't need the decodeURIComponents() on the name, just the gmnotes.
December 11 (7 years ago)
Spren
Sheet Author
There's an old script I really like called tokenotes that may be something you would like. It basically makes the GM notes show up on the board in text. So all you have to do is single click to see any notes. It's one of those things that should be a roll20 default feature but for some reason isn't.

https://app.roll20.net/forum/post/1760741/script-tokenotes
December 11 (7 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

The Aaron said:

Hmm.  You mean whenever there are quotes in the notes field? Probably it could happen on mine as well.  decodeURIComponent can throw exceptions but I don't know what would cause that to happen.  If you have an example of a token where it happens, I'd be happy to come figure out the precise issue. 

BTW, You don't need the decodeURIComponents() on the name, just the gmnotes.

I'll come up with some samples tonight. I have to pretend to be at work right now...
December 11 (7 years ago)

Edited December 11 (7 years ago)
Is this the error you were refering to? By the way, I am going to go this route.

URIError: URI malformed
URIError: URI malformed
at decodeURIComponent (<anonymous>)
at _.chain.map.reject.reject.each.o (apiscript.js:419:59)
at Function._.each._.forEach (/home/node/d20-api-server/node_modules/underscore/underscore.js:153:9)
at _.(anonymous function) [as each] (/home/node/d20-api-server/node_modules/underscore/underscore.js:1496:34)
at apiscript.js:414:18
at eval (eval at <anonymous> (/home/node/d20-api-server/api.js:146:1), <anonymous>:65:16)
at Object.publish (eval at <anonymous> (/home/node/d20-api-server/api.js:146:1), <anonymous>:70:8)
at /home/node/d20-api-server/api.js:1510:12
at /home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:560
at hc (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:39:147)

For reference, this is the text it's trying to read

16B. Northeast Room

This room holds an iron-framed bed, two cabinets (one containing manacles, the other containing instruments of torture), a barrel of water, a barrel of dwarven ale, two empty ironbound wooden crates, and an iron trunk with a lock built into it. The trunk is 7 feet tall, 13 feet long, and 8 feet wide, and it weighs 1,000 pounds. The fire giant in area 12 carries the key to the lock, which is too big to be picked with thieves’ tools. A Small or Medium character can reach into it and open it with a successful DC 20 Dexterity check.

Treasure. The trunk contains 15,000 cp, 6,200 sp, 700 gp, a drinking horn made from a gorgon’s horn and bearing flame-like patterns (worth 2,500 gp and weighing 50 pounds), and a sack containing 2d4 mundane items, determined by rolling on the Items in a Giant's Bag table in the introduction.
December 12 (7 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Yes. In order to avoid the problem, the apostrophes need to be removed.
December 12 (7 years ago)
Does the method you provide handle this?
December 12 (7 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
No, I manually remove them or avoid typing them in the first place. If you type the apostrophe directly in the GM note's field, you get a safe character (the foot mark, or, ' ), if you paste it in from some source that gives typographer's quotes or real apostrophes (’) it crashes. So I am careful to replace them before using any pasted content.

The section you provided as a sample had both kinds. The "Giant's Bag" used a straight apostrophe (the foot mark), the "gorgon’s horn" (and others) had a real apostrophe and would cause an API crash.
December 12 (7 years ago)
The Aaron
Pro
API Scripter
Ah... Unicode... my old nemesis...
December 12 (7 years ago)

Edited December 12 (7 years ago)
Jakob
Sheet Author
API Scripter
Don't you want to join the lines by a HTML line break (<br>) instead of a carriage return (\r)?

Anyway, getting a completely safe string is probably hard, but at least you can get rid of quotes and emdashes:
o.get('gmnotes').replace(/(?:%u2018|%u2019)/g, "'").replace(/(?:%u201C|%u201D)/g, '"').replace(/(?:%u2013|%u2014)/g, '-')
EDIT: Edited to adapt to how Roll20 actually stores these characters.
December 12 (7 years ago)
Yeah, it's because I'm copy pasting it. This is just another example of why they need to handle mapnotes way better in roll20.
December 12 (7 years ago)

Jeremy R. said:

Yeah, it's because I'm copy pasting it. This is just another example of why they need to handle mapnotes way better in roll20.

Can't agree more... even allowing a token to be linked to a handout so that I can shift+double click to bring up the handout would be so much better.
December 12 (7 years ago)
Spren, thanks for passing along the link to that older thread.  TokeNotes is perfect for what I need.
December 12 (7 years ago)
The Aaron
Pro
API Scripter
Try this function:
const fixUnicode = (str) => {
    const replacers = {
        "[\u2018\u2019]"   : "'",
        "[\u201C\u201D]"   : '"',
        "[\u2013\u2014]"   : '-',
        "[\u2026]"         : '...',
        "[^\u0000-\u007f]" : ''
    };
    return Object.keys(replacers).reduce( (s,r)=>s.replace(RegExp(r,'g'),replacers[r]),str);
};

/* ... */


/* ... */ decodeURIComponent(fixUnicode(o.get('gmnotes'))  /* ... */
The replacers object contains the regular expressions for specific unicode first (I just took what Jakob had) and their ASCII equivalents, and the final line matches anything left and gets rid of it.

December 12 (7 years ago)
Jakob
Sheet Author
API Scripter
Aaron, you probably didn't see my edited version - you have to replace the string "%u2018", not the unicode character "\u2018".
December 12 (7 years ago)
The Aaron
Pro
API Scripter
Ah, good point.  I was dealing with the resultant text rather than the stored text.  That does complicate things slightly. =D  I suppose I'll have to test this out some.
December 12 (7 years ago)
The Aaron
Pro
API Scripter
Ok, thanks for that info Jakob, that actually leads to an even better solution!  The decodeURIComponent() function in the API sees the % and tries to turn the next 2 characters into ascii, but doesn't know how to decode a 'u'.  The solution is to handle all the unicode decoding first:
    const decodeUnicode = (str) => str.replace(/%u[0-9a-fA-F]{2,4}/g,(m)=>String.fromCharCode(parseInt(m.slice(2),16)));
Then decodeURIComponent() can take care of the rest and you don't have to care about the unicode because it displays fine:
/* ... */ decodeURIComponent(decodeUnicode(o.get('gmnotes'))  /* ... */
December 12 (7 years ago)

Edited December 12 (7 years ago)
Edit to my original question. I've put the whole thing back together and so far it seems to be working quite well. I've listed it all in one piece here for reference.

const decodeUnicode = (str) => str.replace(/%u[0-9a-fA-F]{2,4}/g,(m)=>String.fromCharCode(parseInt(m.slice(2),16)));


on('ready',function(){
    'use strict';


    on('chat:message',function(msg){
        if('api' === msg.type && msg.content.match(/^!gmnote/) && playerIsGM(msg.playerid) ){
            let match=msg.content.match(/^!gmnote-(.*)$/),
                regex;
            if(match && match[1]){
                regex = new RegExp(`^${match[1]}`,'i');
            }
                                
            _.chain(msg.selected)
                .map( s => getObj('graphic',s._id))
                .reject(_.isUndefined)
                .reject((o)=>o.get('gmnotes').length===0)
                .each( o => {
                    if(regex){
                        let lines=_.filter(decodeURIComponent(decodeUnicode(o.get('gmnotes'))).split(/(?:[\n\r]+|<br\/?>)/),(l)=>regex.test(l)).join('\r');
                        sendChat(o.get('name'),`/w gm ` + lines);
                    } else {
                        sendChat(o.get('name'),`/w gm ` + decodeURIComponent(decodeUnicode(o.get('gmnotes'))));
                    }
                });
        }
    });
});
December 12 (7 years ago)

Edited December 12 (7 years ago)
Also, I'm just curious as to why this if statement is here, as in what task it's checking for or performing.

                .each( o => {
                    if(regex){
                        let lines=_.filter(decodeURIComponent(o.get('gmnotes')).split(/(?:[\n\r]+|<br\/?>)/),(l)=>regex.test(l)).join('\r');
                        sendChat(o.get('name'),`/w gm &{template:desc} {{desc=` + lines + `}}`);
                    } else {
                        sendChat(o.get('name'),`/w gm &{template:desc} {{desc=` + decodeURIComponent(o.get('gmnotes')) + `}}`);
                    }
                });
I ask, because I noticed that Keith's code does not contain this check. Also sorry for a lot of the questions. I'm used to just going to stack exchange for c# :P.
December 12 (7 years ago)
The Aaron
Pro
API Scripter
This block:
        if('api' === msg.type && msg.content.match(/^!gmnote/) && playerIsGM(msg.playerid) ){
            let match=msg.content.match(/^!gmnote-(.*)$/),
                regex;
            if(match && match[1]){
                regex = new RegExp(`^${match[1]}`,'i');
            }
checks if the chat command begins with !gmnote, then grabs anything after !gmnote- and makes a regular expression to match lines starting with it.  If that regex has been created, it will instead find all the lines matching it and output only those.  So, you could do:
!gmnotes-gold
and it would find all the lines that start with gold (case insensitive) and output them.

December 12 (7 years ago)
The Aaron
Pro
API Scripter
I think that was an addition I did for someone, probably Ziechael. =D
December 12 (7 years ago)
Ziechael
Forum Champion
Sheet Author
API Scripter
Sounds like something I'd ask for... maybe as part of the Token GM-note chat editor I pestered you about :)
December 12 (7 years ago)
So, one last thing, and I'm quite sure this isn't possible because we can't use the api for interface actions.

Is there anyway to intercept a different style click on a token (ie. ctrl + click or a hotkey). Then, maybe a separate thing, read a link from the GM notes or a bar, and then try to open that link. I'm thinking with the API maybe we get around this link to handouts issue another way. Put the link in the token somewhere and just automatically open it. I know you can print the link from the sheet to chat, and then click on it there. I'm looking to bypass a step here.
December 12 (7 years ago)
The Aaron
Pro
API Scripter
Nope.  I suggested that about 3-4 years ago in the old suggestions forum, but it hasn't come up yet...
December 12 (7 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

The Aaron said:

This block:
        if('api' === msg.type && msg.content.match(/^!gmnote/) && playerIsGM(msg.playerid) ){
            let match=msg.content.match(/^!gmnote-(.*)$/),
                regex;
            if(match && match[1]){
                regex = new RegExp(`^${match[1]}`,'i');
            }
checks if the chat command begins with !gmnote, then grabs anything after !gmnote- and makes a regular expression to match lines starting with it.  If that regex has been created, it will instead find all the lines matching it and output only those.  So, you could do:
!gmnotes-gold
and it would find all the lines that start with gold (case insensitive) and output them.

I'll edit that into my copy. It looks like a useful addition. Cool!

December 13 (7 years ago)
GiGs
Pro
Sheet Author
API Scripter
I've been trying the completed script Jeremy R posted above, using
!gmnotes-gold
and it is just outputting the complete text, even though i have a line starting with gold.

To be clear, I am seeing identical behaviour when using 
!gmnotes-gold
and
!gmnotes
Both just output the full contents of the GMnotes.
What am I missing?

December 13 (7 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I'm having the opposite problem. The filter works fine, but I'm still crashing on an apostrophe.
December 13 (7 years ago)
The Aaron
Pro
API Scripter
G G: Looks like I mistyped it above. it's !gmnote-gold, not gmnotes-gold.  Making sure it works now...

Keithcurtis: hmm.... might have to look at that in-game.
December 13 (7 years ago)
The Aaron
Pro
API Scripter
So, one problem is that if you've got HTML in there, it likely doesn't split well.  I'm throwing together a simple block level element splitter to reflow the HTML before breaking into "lines", then I'll make the matching ignore html and it should output lines fine.  Initial testing did a pretty good job, but It wasn't accounting for inline elements like <b> that you might want to retain in the same "line".
December 13 (7 years ago)
GiGs
Pro
Sheet Author
API Scripter
Thanks, Aaron, the gmnote-gold works. 
December 14 (7 years ago)
The Aaron
Pro
API Scripter
Ok. Here's a version that will at least take a stab at block level elements of HTML.  I also expanded the command slightly to provide a whispered and open version:
  • !gmnote
  • !wgmnote
  • !gmnote-<some text>
  • !wgmnote-<some text>
It will do it's best to preserve the formatting, but short of a full HTML parser, it can't be perfect.  YMMV.
on('ready',function(){
    'use strict';

    const decodeUnicode = (str) => str.replace(/%u[0-9a-fA-F]{2,4}/g,(m)=>String.fromCharCode(parseInt(m.slice(2),16)));

    const blockElements = [
        'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'pre', 'address',
        'blockquote', 'dl', 'div', 'fieldset', 'form', 'hr', 'noscript', 'table','br'
    ];
    const rStart=new RegExp(`<\\s*(?:${blockElements.join('|')})\\b[^>]*>`,'ig');
    const rEnd=new RegExp(`<\\s*\\/\\s*(?:${blockElements.join('|')})\\b[^>]*>`,'ig');

    const getLines = (str) => 
        (rStart.test(str)
            ? str
                .replace(/[\n\r]+/g,' ')
                .replace(rStart,"\r$&")
                .replace(rEnd,"$&\r")
                .split(/[\n\r]+/)
            : str
                .split(/(?:[\n\r]+|<br\/?>)/)
            )
            .map((s)=>s.trim())
            .filter((s)=>s.length)
            ;
    const cmdRegex = /^!(w?)gmnote(?:-(.*))?$/i;

    on('chat:message',function(msg){
        if('api' === msg.type && cmdRegex.test(msg.content) && playerIsGM(msg.playerid) ){
            let match=msg.content.match(cmdRegex),
                output = match[1].length ? '/w gm ' : '',
                regex;

            if(match[2]){
                regex = new RegExp(`^${match[2]}`,'i');
            }
                          
            _.chain(msg.selected)
                .map( s => getObj('graphic',s._id))
                .reject(_.isUndefined)
                .reject((o)=>o.get('gmnotes').length===0)
                .each( o => {
                    if(regex){
                        let lines = _.filter(
                            getLines(decodeURIComponent(decodeUnicode(o.get('gmnotes')))),
                            (l) => regex.test(l.replace(/<[^>]*>/g,''))
                        ).join('<br>');
                        sendChat(o.get('name'), `${output}${lines}`);
                    } else {
                        sendChat(o.get('name'), `${output}${decodeURIComponent(decodeUnicode(o.get('gmnotes')))}`);
                    }
                });
        }
    });
});

December 14 (7 years ago)
GiGs
Pro
Sheet Author
API Scripter
Cool, I was thinking about asking for an open chat version, too.
December 14 (7 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Sweet!