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.