
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; } }); });