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

[Script] PermitFolder — Recursive permission adjustments to handouts and characters.

1530590521

Edited 1530590623
The Aaron
Pro
API Scripter
This is just a simple snippet, really.  It adds and removes permissions for players to view and edit handouts and characters by folder, recursively. Commands: !permit-folder [Options] --[Folder Name] --[Player Name|all] --[Player Name] ... Where Options can be one or more of the following: --view or --no-view : this adds or removes permission for the players to view all the items in the specified folder and below. --edit or --no-edit : this adds or removes permission for the players to edit/control all the items in the specified folder and below. In the case of an ambiguous folder name, it will prompt for which one you mean with buttons to continue.  Folder names need to be the full name, but are case insensitive.  Player names can be partials (will match all who contain the string) or Player IDs, or Roll20 User IDs. Here are some examples: !permit-folder --view --PCs --all Allow all players to view the characters and handouts in the PCs folder and below !permit-folder --view --no-edit --Bob's Characters --Tom Allow view but remove edit/control access to all characters and handouts in Bob's Characters and below to any player with Tom in their name. !permit-folder --edit --Aaron's things --104025 Give edit permission to Roll20 User 104025 (hey, that's me!) !permit-folder --view --Gallery --Bob --104025 ---J_ElTmce_efJaNmwIWJ Grand view permission to Gallery and below to me, all bobs, and the player who's id is -J_ElTmce_efJaNmwIWJ (note the extra -). Hope this is helpful! Script: on('ready', () => { const pathSep = ' > '; const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g,'\\$1'); const HE = (() => { const e = (s) => `&${s};`; const entities = { '<' : e('lt'), '>' : e('gt'), "'" : e('#39'), '@' : e('#64'), '{' : e('#123'), '|' : e('#124'), '}' : e('#125'), '[' : e('#91'), ']' : e('#93'), '"' : e('quot') }; const re = new RegExp(`(${Object.keys(entities).map(esRE).join('|')})`,'g'); return (s) => s.replace(re, (c) => (entities[c] || c) ); })(); const buildLookup = (data) => { const dataTree=JSON.parse(data||'{}'); const lookup = {}; const treeBuilder = (obj,path) => { obj.forEach((n) => { if(_.isString(n)){ let p = [...path]; p.reduce((m,a)=>{ m.push(a); let curPathKey = m.join(pathSep); lookup[curPathKey]= (lookup[curPathKey]||[]); lookup[curPathKey].push(n); return m; },[]); } else { path.push(n.n); treeBuilder(n.i,path); path.pop(); } }); }; treeBuilder(dataTree,[]); return lookup; }; const resolvePlayers = (players) => { let playerIDs = []; if(players.length) { const reName = new RegExp(`(${players.map(p=>esRE(p)).join('|')})`,'i'); playerIDs = findObjs({ type: 'player' }).filter( p => players.includes(p.id) || players.includes(p.get('d20userid')) || reName.test(p.get('displayname')) ) .map( p => p.id); } return playerIDs; }; const resolveFolder = (name,exact) => { const re = new RegExp(`${exact?'^':`(^|${esRE(pathSep)})`}${esRE(name)}$`,'i'); let locationLookup=buildLookup(Campaign().get('journalfolder')); let keys = Object.keys(locationLookup).filter((k)=>re.test(k)); return keys.reduce((m,k)=>{ m[k]=locationLookup[k]; return m;},{}); }; const disambiguate = (who, folderNames, fmt) => { sendChat('Permit Folder', `/w "${who}" <b>Which one</b>: <ul>${folderNames.map((n)=>`<li><a href="${fmt(HE(n))}">${HE(n)}</a></li>`).join('')}</ul>`); }; const sendHelp = (who, error) => { sendChat('Permit Folder', `/w "${who}" <div>${error ? `<div><b>${error}</b></div>`:''}<div><code>!permit-folder --[Options] --[Folder Name] --[Player Name|all] --[Player Name] ...</code></div><div>Where <code>[Options]</code> is one or more of:<ul><li><code>--view</code> or <code>--no-view</code> -- this adds or removes view permissions for the players on all items in the specified folder and below.</li><li><code>--edit</code> or <code>--no-edit</code> -- this adds or removes edit permissions for the players on all items in the specified folder and below.</li></ul></div></div>`); }; const csv2a = (s) => s.split(/,/).filter(s2 => s2.length); const addToField = (o,f,v) => o.set(f,[...new Set([...csv2a(o.get(f)),...v])].join(',')); const removeFromField = (o,f,v) => o.set(f, csv2a(o.get(f)).filter(a=>!v.includes(a)).join(',')); const sendConfirm = (who,count) => sendChat('Permit Folder', `/w "${who}" Adjusted permissions on ${count} journal entr${count===1?'y':'ies'}.`); on('chat:message', (msg) => { if('api' !== msg.type || !playerIsGM(msg.playerid) ){ return; } let args = msg.content.split(/\s+--/); switch(args.shift().toLowerCase()){ case '!permit-folder': { const who=(getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); let addView = false; let addEdit = false; let removeView = false; let removeEdit = false; let all = false; let exact = false; let folderName; let players = []; args.forEach(a => { switch(a){ case 'view': addView=true; break; case 'no-view': removeView=true; break; case 'edit': addEdit=true; break; case 'no-edit': removeEdit=true; break; case 'exact': exact=true; break; case 'all': all = true; break; default: if(!folderName){ folderName = a; } else { players.push(a); } } }); if( (addView || addEdit || removeView || removeEdit) && !(addView && removeView) && !(addEdit && removeEdit)) { let playerIDs = resolvePlayers(players); if(all) { playerIDs.push('all'); } if(playerIDs.length){ let folders = resolveFolder(folderName,exact); if(Object.keys(folders).length > 1){ disambiguate(who, Object.keys(folders), (f)=>`!permit-folder ${addView?'--view ':''}${removeView?'--no-view ':''}${addEdit?'--edit ':''}${removeEdit?'--no-edit ':''} --exact --${f} ${players.map((p)=>`--${p}`).join(' ')}`); } else if(Object.keys(folders).length) { let key = Object.keys(folders)[0]; let ids = folders[key]; const manip = (obj) => { if(addView){ addToField(obj,'inplayerjournals',playerIDs); } if(removeView){ removeFromField(obj,'inplayerjournals',playerIDs); } if(addEdit){ addToField(obj,'controlledby',playerIDs); } if(removeEdit){ removeFromField(obj,'controlledby',playerIDs); } }; let objs = [ ...findObjs({type:'character'}).filter(c => ids.includes(c.id)), ...findObjs({type:'handout'}).filter(h => ids.includes(h.id)) ]; let count =objs.length; objs.forEach(manip); sendConfirm(who, count); } else { sendHelp(who, `No folder found for <code>${folderName}</code>`); } } else { sendHelp(who, players.length ? `No players found for <code>${players.join(', ')}</code>.` : `No players specified.`); } } else { sendHelp(who, `Missing or conflicting options.`); } } break; } }); }); Support my work on If you use my scripts, want to contribute, and have the spare bucks to do so , go right ahead. However, please don't feel like you must contribute just to use them! I'd much rather have happy Roll20 users armed with my scripts than people not using them out of some sense of shame. Use them and be happy, completely guilt-free! Disclaimer: This Patreon campaign is not affiliated with Roll20; as such, contributions are voluntary and Roll20 cannot provide support or refunds for contributions.
1530590846

Edited 1530590904
vÍnce
Pro
Sheet Author
Cool addition Aaron.  Does this only assign permissions to existing handouts and journals within the folder and below, or will it auto-assign permissions to anything placed within the permitted folder and below as long as the script is present/running?
1530591322
The Aaron
Pro
API Scripter
Currently, it makes a one-time change. That’s a cool idea though, where were you when I was writing this!? =D Tell me what else you’re thinking, probably this will get expanded at some point to do more...
1530592564
vÍnce
Pro
Sheet Author
I "borrowed" your idea of creating a folder for each player that includes a "Magic Items" sub-folder where I can drag magical item handouts. Currently I set permissions per handout, manually.  It would be nice to just set permission on the root and have whatever I drag to the player's folder automatically set the proper permission.
1530594447
The Aaron
Pro
API Scripter
Ok. You’ve convinced me. This is going to have to become a full fledged script with state and persisted permissions...
1530595316
vÍnce
Pro
Sheet Author
The Aaron said: Ok. You’ve convinced me. This is going to have to become a full fledged script with state and persisted permissions... Sorry. Not sorry.  ;-P
1530644471
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Aaron, can this be used to do some more complicated things as well? Like say I want everyone in a game to have access to unfoldered items, but a certain folder should be only viewable by the gm (essentially in no one's journal). And then, also seconding the update as changes happen suggestion from vince. I'll also add to it that if it can, or becomes able to, control access to the unfoldered items, it should also run a check at startup to handle characters that got vaulted in while the sandbox was down.
1530646164
The Aaron
Pro
API Scripter
When you say "unfolded", you're talking about the +/- expanded vs contracted folders?  That's not data that's accessible to the API.
1530656123

Edited 1530656144
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
nope, the items that aren't in any folder. Not sure why I felt the need to make up words for that concept.
1530661132
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I have a pretty good idea of what to do with this one. :) Neat idea. Live updating will make it even cooler.
Interesting idea! :)