Script - unWhisper v0.1.0 Thanks to Covid, I have some time to kill, so I bashed this together after seeing a post about it earlier. It's pretty rough, and probably has some bugs. I also don't use Roll20 for games these days, so.... I may have done some stupid things. It seems to work though? This script allows the GM to unwhisper a recent self-whisper and send it public. The script only stores the GM's own whispers (ie private rolls made from sheets/macros), not private whispers from other players. Commands (not case sensitive): !unWhisper -alias for !unWhisper --0, this will publicly post your most recent whisper !unWhisper --list -list all stored messages in chat, 0-indexed, with 0 being the most recent. Each message has a small preview made up from the roll template name (if applicable) and either a {{*name=}} template property, or the start of the message text. !unWhisper --X -where X is a number. Post the message with the index 'X'. Same as clicking on the buttons in --list. !unWhisper --maxWhispers -set the number of whispers to store. Default is 5, max is 30 !unWhisper --maxPreview -set the max length of the message preview in the --list view. Default is 20 characters, max is 50. IMPORTANT - This script relies on Tim & Aaron's libInline to restore inline rolls to roll templates. Install it from the one-click if you want to see original rolls (though you won't get original tooltips or Quantum roll icons). /* globals log on playerIsGM, state, sendChat, libInline */ const unWhisper = (() => { //eslint-disable-line no-unused-vars const scriptName = 'unWhisper'; const version = { M: 0, m: 1, p: 0, get: function() { return `${this.M}.${this.m}.${this.p}` }, getFloat: function() { return parseFloat(`${this.M}.${this.m}${this.p}`) } } const config = { maxWhispers: 5, maxPreviewSize: 20, } const init = () => { if (!state.unWhisper || !state.unWhisper.version) { state.unWhisper = { version: version.getFloat(), whispers: [], config: config, } } else if (state.unWhisper.version < version.getFloat) { // update version } refreshConfig(); on('chat:message', handleInput); log(`- Initialised ${scriptName} - v${version.get()} -`); setTimeout(() => { if (!/object/i.test(typeof(libInline))) return doChat(`/w gm <div style="color: red; font-weight: bold">libInline was not found: Please install from one-click library to enable inline rolls!</div>`, 'gm'), 250 }); } const refreshConfig = () => Object.assign(config, state.unWhisper.config); const filterPreview = (inputStr) => { let output = `${inputStr}`.replace(/[{}[\]@]/g, ''); return output; } const listWhispers = () => { const headStyle = `background-color: black; color: white; font-weight:bold; border:2px solid black; width: 100%; word-break: break-all; line-height: 2em;` const rowStyle = `background: white; color: black; font-weight: normal; padding: 5px 2px 5px 2px; line-height: 2rem; word-break: break-all` const buttonStyle = `background-color: darkblue; border: 1px darkblue solid; padding: 2px 5px 2px 5px; border-radius:3px; color: white; font-weight: bold; ` let previews = state.unWhisper.whispers.map(w => w.preview || 'no preivew'), templateHead = `<div style="${headStyle}">&nbsp;&nbsp;&nbsp;unWhisper`, templateBody = [], templateFoot = `</div>` for (let i = previews.length - 1; i >= 0; i--) { templateBody.push(`<div style="${rowStyle}"><a href="!unWhisper --${i}" style="${buttonStyle}">Msg ${i}</a> ${previews[i]}</div>`); } //(`{{[Msg ${i}](\`!unWhisper --${i}" style="${buttonStyle})=${previews[i]}}}`); } doChat(`${templateHead}${templateBody.join(' ')}${templateFoot}`, 'gm'); } const storeWhisper = (whisper) => { let store = state.unWhisper.whispers, header = whisper.rolltemplate ? `\*\*${whisper.rolltemplate}\*\*/` : `\*\*-\*\*/`, // eslint-disable-line no-useless-escape nameMatch = whisper.content.match(/name=([^}]+?)}/); header += nameMatch ? nameMatch[1] : whisper.content.slice(0, config.maxPreviewSize); store.unshift({ preview: filterPreview(header), msg: whisper }); while (store.length > config.maxWhispers) { store.pop(); } // log(`Stored whisper - "${header}" - ${store.length} in store.`); } const sendWhisper = (index = 0) => { const whisper = state.unWhisper.whispers[index]; if (!whisper || !whisper.msg) return log(`unWhisper: bad index "${index}"`); const templatePrefix = whisper.msg.rolltemplate ? `&{template:${whisper.msg.rolltemplate}} ` : ''; let newContent = `${templatePrefix}${whisper.msg.content}`; if (whisper.msg.inlinerolls && whisper.msg.inlinerolls.length) { const rolls = libInline.getRollData(whisper.msg.inlinerolls); newContent = newContent.replace(/\$\[\[(\d+)]]/g, ((m, p1) => rolls[p1].getRollTip())); } doChat(newContent); } const doChat = (msg, who) => { const whisper = who ? `/w "${who}" ` : ``; sendChat(scriptName, `${whisper}${msg}`, null, { noarchive: true }); } const handleInput = (msg) => { if (!playerIsGM(msg.playerid)) return; if (msg.type === 'api' && /^!unwhisper/i.test(msg.content)) { let line = (msg.content.match(/^!unwhisper\s+(.+)/i) || [])[1]; if (!line) sendWhisper(0); else { let params = line.split(/\s*--\s*/g); params.shift(); params.forEach(param => { let cmd = (param.match(/^([^\s]+?)(\s|$)/) || [])[1], args = (param.match(/\s+(.+)/) || [])[1], change; if (!cmd) return; if (/maxwhisp/i.test(cmd)) { let newMax = args.replace(/\D/g, ''); if (newMax > 0 && newMax < 30) { state.unWhisper.config.maxWhispers = newMax; change = `new maxWhispers: ${newMax}`; } } else if (/maxprev/i.test(cmd)) { let newMax = args.replace(/\D/g, ''); if (newMax > 0 && newMax < 50) { state.unWhisper.config.maxPreviewSize = newMax; change = `new maxPreview: ${newMax}`; } } else if (/list/i.test(cmd)) { listWhispers(); } else if (/^\d+/.test(cmd)) { let index = cmd.replace(/\D/g, ''); sendWhisper(index); } else doChat(`Unrecognised command: "${cmd}"`, 'gm'); if (change) { doChat(`Setting change: ${change}`, 'gm'); refreshConfig(); } }); } } else if (msg.target && (/^gm$/i.test(msg.target) || playerIsGM(msg.target)) && playerIsGM(msg.playerid)) { storeWhisper(msg); } }; on('ready', () => init()); })();