rh test7
Author: rubbinh00d
Description Source Code Launch Bot Current Users

Short Description:

something something dark side

Full Description

The purpose of this app is to demonstrate the use of a data saving add-on for Chaturbate apps and bots.
 
The add-on consists of two components:

a javascript module that adds a data save and restore capability to an app or bot, and

a javascript applet for broadcasters to control the save and restore process for any apps and bots they use that include the module and that have been configured to use it.


App and bot data can be saved to, and restored from, either


browser Local Storage, or

the broadcaster's local File System.


The advantage of using Local Storage is that data saves can be automated. The disadvantage of using local storage is that the saved data is less permanent than that saved to the File System. Local storage can be wiped automatically, if system resources are running low, or if cached data is cleared by the user. Indeed, if the browser is used in so-called Private or Incognito mode, local storage is wiped every time the browser window is closed.
 
CBS-enabled Tip Goal
This app is a copy of the popular sample app script Tip Goal by Admin that includes the CBS module and has been configured to use it.
 
CBS Save'N'Restore Applet
In order to access the added save and restore feature, a broadcaster must run the following javascript applet:


javascript:/*** CB app/bot data Save'n'restore bookmarklet v2.010 ***/window.__nativeST__=window.__nativeST__||window.setTimeout;window.__nativeSI__=window.__nativeSI__||window.setInterval;window.setTimeout=function(a,k){var g=this,u=Array.prototype.slice.call(arguments,2);return window.__nativeST__(a instanceof Function?function(){a.apply(g,u)}:a,k)};window.setInterval=function(a,k){var g=this,u=Array.prototype.slice.call(arguments,2);return window.__nativeSI__(a instanceof Function?function(){a.apply(g,u)}:a,k)};

(function(a,k,g,u){function f(b,c){c=c||0;b[c++](function(b,c){return function(){c<b.length&&a.setTimeout(function(){f(b,c)},1)}}(b,c))}a.CBS={version:"CBS::v2::CB app/bot data Save/restore::20171104.010::Release"};g=g||"1";u=u||function(a,b,c,d,g){setTimeout(g,1E4,a,b,c,d)};var b=k.getElementById("CBSv2Overlay"),n=[],c,q=!1,d,p=!1,E,w;/(camgasm|chaturbate)\.com$/i.test(k.location.hostname)?b||f([function(a){var c="<style>.CBSv2-no-overflow {overflow:hidden!important;outline:0;}${visibility:visible;position:fixed;top:0;right:0;left:0;bottom:0;overflow-y:auto;padding:0;z-index:10;background-color:rgba(0,0,0,0.05);}$ div {visibility:visible;width:300px;margin:100px auto;background-color:#fff;border:1px solid #000;padding:15px;text-align:center;}</style>".replace(/\$/g,

"#CBSv2Overlay");b=k.createElement("div");b.setAttribute("id","CBSv2Overlay");b.style.display="block";b.innerHTML=c+"<div>Starting...</div>";k.body.appendChild(b);k.body.classList.add("CBSv2-no-overflow");n.push(b);setTimeout(function(){a()},1E3)},function(d){(c=a.jQuery)&&g<=c.fn.jquery?d():(b=k.createElement("script"),b.type="text/javascript",b.src="//ajax.googleapis.com/ajax/libs/jquery/"+g+"/jquery.min.js",b.onload=b.onreadystatechange=function(){w=this.readyState;q||w&&"loaded"!==w&&"complete"!==

w||((c=a.jQuery).noConflict(q=!0),b.onload=b.onreadystatechange=null,d())},k.documentElement.childNodes[0].appendChild(b))},function(a){[["//cdn.jsdelivr.net/alertifyjs/1.4.1/css/alertify.min.css","alertify-notifier ajs-left","left","10px"],["//cdn.jsdelivr.net/alertifyjs/1.4.1/css/themes/default.min.css"],["//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css","fa","font-family","FontAwesome"]].forEach(function(a){(function(a,b,d,g){b=c("<div>").hide().css({height:0,width:0}).addClass(b||

"").appendTo("body");d=b.css(d||"")===g;b.remove();return c('link[rel="stylesheet"][href$="'+a.match(/(?:\/)[^\/]+$/)+'"]').length&&d}).apply(null,a)||(b=k.createElement("link"),b.setAttribute("rel","stylesheet"),b.setAttribute("href",a[0]),k.head.appendChild(b))});a()},function(c){(d=a.alertify)&&d.dialog?c():(b=k.createElement("script"),b.src="//cdn.jsdelivr.net/alertifyjs/1.4.1/alertify.min.js",b.onload=b.onreadystatechange=function(){w=this.readyState;p||w&&"loaded"!==w&&"complete"!==w||(E=a.alertify,

a.alertify=d,d=E,p=!0,b.onload=b.onreadystatechange=null,c())},k.documentElement.childNodes[0].appendChild(b))},function(a){setTimeout(function(){var a=k.querySelector("#CBSv2Overlay div");a&&(a.style.visibility="hidden")},3E3);a()},function(a){u(c,q,d,p,a)},function(a){p&&setTimeout(function(){Array.prototype.slice.call(k.querySelectorAll('div[class*="alertify-"],div[class*="ajs-"]')).forEach(function(a){a.parentElement.removeChild(a)})},2E3);a()},function(a){n.forEach(function(a){a.parentNode.removeChild(a)});

k.body.classList.remove("CBSv2-no-overflow");a()}]):a.alert("*chaturbate.com only bookmarklet")})(window,document,"1.6.4",function(a,k,g,u,f){function b(){return window.ws_handler&&window.ws_handler.connected||window.websocket_handler&&window.websocket_handler.connected||window.flash_handler&&window.flash_handler.connected||window.html_handler&&window.html_handler.connected}function n(e){v.empty().append(P,a("<p>"+e+"</p>"));z&&z.isOpen()&&z.setContent(v[0]).set("closable",!0)}function c(){z&&z.isOpen()&&

z.close().set("closable",!0)}function q(a){g.success(a).callback=c}function d(a){g.error(a).callback=c}function p(e){var b=L[window.CBSv2_ping.split("/").length],l;window.$message_sender.confirmed_send=!0;void 0!==(l=window.flash_handler)&&l.connected?(l.consolelog("User is sending message: "+e),e=a.toJSON({m:e,c:"",f:""}),l.consolelog(e),window.GetFlashObject("movie").SendRoomMsg(e)):void 0!==(l=window.html_handler)&&l.connected?(l.consolelog("User is sending message: "+e),e=a.toJSON({m:e,c:"",f:""}),

l.consolelog(e),a.ajax({url:l.post_address,dataType:"json",data:{room:l.room_owner_nick,message:e,username:l.user},type:"POST",success:function(a){if(""!==a&&a["X-Spam"])l.message_inbound.on_room_message(l.user,e)}})):void 0!==(l=window.websocket_handler)&&l.connected?l.connection.send(JSON.stringify({action:"msg",msg:e})):void 0!==(l=window.ws_handler)&&l.connected?(l.consolelog("User is sending message: "+e),e=a.toJSON({m:e,c:"",f:""}),l.consolelog(e),l.SendRoomMsg(e)):window.CBSv2_autosave_interval?

m.enm():n(b+" Failed. Chat may be disconnected.")}function E(){var e=a('.stop_link[name="/app/stop/'+this.idx+'/"]');this[this.idx]=e.length?e[0].parentNode.previousElementSibling.innerHTML.trim():"";this[this.idx].length&&b()?(window.CBSv2_ping=["","#"+this.idx+"CBSv2",G,""].join("/"),p(window.CBSv2_ping),A=setTimeout.call(this,function(){this[this.idx]="";this.enm()},5E3)):this.enm()}function w(){var a=L[window.CBSv2_ping.split("/").length];A=null;window.CBSv2_autosave_interval?m.enm():n(a+" Failed. Chat may be unresponsive.")}

function J(a,b){b=b||0;var e=y.slice(b,b+512);window.CBSv2_ping=["","#"+a+"CBSv2",G,b,e,e.length,""].join("/");p(window.CBSv2_ping);A=setTimeout(w,5E3)}function K(a,b){window.CBSv2_ping=["","#"+a+"CBSv2",G,b||0,""].join("/");p([window.CBSv2_ping,""].join("/"));A=setTimeout(w,5E3)}function U(){this[this.idx].length&&b()&&window.CBSv2_autosave_interval?K(this.idx):this.enm()}function x(e){function b(){x.a?--x.b?setTimeout(b,1E3):e.call(this):e.call(this)}window.CBSv2_autosave_interval?x.a?e&&(x.b=5,

setTimeout(b,1E3)):(x.a=!0,a("#CBSv2Overlay").length||m.enm(E,function(){G=(new Date).valueOf();m.enm(U,function(){x.a=!1;e&&e.call(this)})})):e&&e.call(this)}function O(){var a,b;r=[];for(b=0;b<t.length;b++){for(a=t.key(b).split("/");4<a.length;)a[1]=[a[1]].concat(a.splice(2,1)).join("/");4===a.length&&"CBSv2"===a[0]&&r.push(a.concat(t.key(b),[a[1],(new Date(parseInt(a[3],10))).toLocaleString(),B[parseInt(a[2],10)]].join(" ")))}}function V(a,b,l,d,h,M,C){a=parseInt(b[1],10);h?J(a,parseInt(d,10)+

parseInt(C,10)):c()}function W(a){document.body.removeChild(a.target)}function X(a){var b=document.createElement("input");b.type="file";b.onchange=function(b){var e=b.target.files[0],h=new window.FileReader;h.onload=function(b){b=JSON.parse(b.target.result);for(var h=(b.key||"").split("/");4<h.length;)h[1]=[h[1]].concat(h.splice(2,1)).join("/");4===h.length&&"CBSv2"===h[0]&&h[1]===m[a]?(y=b.value,J(a)):alert('"'+e.name+'" does not contain "'+m[a]+'" save data.')};h.readAsText(e,"UTF-8");b.target.parentNode&&

b.target.parentNode.removeChild(b.target)};null===window.webkitURL&&(b.style.display="none",document.body.appendChild(b));b.click()}function Y(a,b,l,g,h){function e(){var a=["CBSv2",m[D],D,l].join("/");if("false"!==t.CBSv2_use_local_storage)t[a]=y,window.CBSv2_autosave_interval?m.enm():q("Saved.");else{var b=new window.Blob([JSON.stringify({key:a,value:y})],{type:"application/json"}),e=document.createElement("a");e.download=a.split("/").slice(0,-1).join("_")+".json";e.innerHTML="Download File";null!==

window.webkitURL?e.href=window.webkitURL.createObjectURL(b):(e.href=window.URL.createObjectURL(b),e.onclick=W,e.style.display="none",document.body.appendChild(e));e.click();c()}}function C(){t.removeItem(r[f][4]);e()}var D=parseInt(b[1],10),f=-1;y="0"===g?h:y+h;if(h)K(D,y.length);else if(y.length)if("false"!==t.CBSv2_use_local_storage){window.CBSv2_autosave_interval&&O();for(a=0;a<r.length;a++)if(b[1]===r[a][2]&&m[D]===r[a][1]){f=a;break}-1<f?window.CBSv2_autosave_interval?C():F("Confirm Overwrite...",

'Overwrite existing "'+r[f][5]+'" saved data?',C,function(){d("Overwrite cancelled.")}):e()}else e();else window.CBSv2_autosave_interval?m.enm():n('No Data. "'+m[D]+'" did not respond with data to save.')}function H(a){A&&(clearTimeout(A),A=null);var b=L[window.CBSv2_ping.split("/").length];a=a.split("/");var e=parseInt(a[1][1],10);""===a[a.length-1]?4===a.length?(m[e]="",m.enm()):n(b+' Failed. "'+m[e]+'" did not respond to "'+b+'" command.'):4===a.length?m.enm():6===a.length?Y.apply(this,a):7===

a.length?V.apply(this,a):window.CBSv2_autosave_interval?m.enm():n(b+' Aborted. "'+m[e]+'" gave an unknown response to "'+b+'" command.')}function Z(a){function b(){d("Delete ALL cancelled.")}var e;z.set("closable",!1);a.html("Processing...");switch(a.attr("class")){case "da":F("Confirm Delete ALL...","Delete ALL saved data?",function(){F("Confirm Confirm Delete ALL...","Are you really sure you want to Delete ALL saved data?",function(){for(e=0;e<r.length;e++)t.removeItem(r[e][4]);q("ALL Deleted.")},

b)},b);break;case "ld":y=t[r[a.data("i")][4]];J(B.indexOf(a.data("s")));break;case "lf":X(B.indexOf(a.data("s")));c();break;case "rm":F("Confirm Delete...",'Delete "'+r[a.data("i")][5]+'" saved data?',function(){t.removeItem(a.data("k"));q("Deleted.")},function(){d("Delete cancelled.")});break;case "sv":K(B.indexOf(a.data("s")))}}function I(a,b){function e(a){var b=window;a=a.split(".");var e,h=a.length;for(e=0;e<h;e++){if(void 0===b||!b.hasOwnProperty(a[e]))return null;b=b[a[e]]}return b}var c=a.split("."),

h=c[c.length-1],d=e(c[0]);c=e(c.slice(0,-1).join("."));if(d&&c&&c.hasOwnProperty(h)&&"function"===typeof c[h]){var g=c[h].CBS_orig;"function"!==typeof g&&(g=c[h]);c[h]=b;c[h].CBS_orig=g;c[h].CBS_root=d}}function N(){a(".stop_link").unbind("click").click(function(){var b=this;x(function(){a.ajax({url:a(b).attr("name"),dataType:"text",data:"",type:"POST",success:function(){a.mydefchatconn("app_tab_refresh")}})})})}function R(a){return'<i class="'+a+'" style="vertical-align:middle; margin-right:20px;"></i>'}

var z,v=a("<div class=CBSv2_buttons></div>"),P=a("<style>.$_buttons{margin:0 auto;padding:10px 20px;}.$_buttons button{display:block;width:100%;margin:5px 0;}.$_buttons input[type=checkbox][disabled] + label{color: #ccc;}</style>".replace(/\$/g,"CBSv2")),B=["Active App","Bot #1","Bot #2","Bot #3"],m=["","","",""],L={4:"Query",6:"Save",7:"Restore"},t=window.localStorage,r,y,A=null,S=/^\/#[0-3]CBSv2\//;window.CBSv2_autosave_interval=window.CBSv2_autosave_interval||null;window.CBSv2_autosave_interval&&

window.clearInterval(window.CBSv2_autosave_interval);window.CBSv2_ping=window.CBSv2_ping||null;Array.prototype.enm=Array.prototype.enm||function(a,b){this.idx=++this.idx||0;arguments.length&&(this.fn=a||null,this.cb=b||null);this.idx<this.length?this.fn&&this.fn.call(this):(delete this.idx,this.cb&&this.cb.call(this))};g.set("notifier","delay",3);I("flash_handler.message_inbound.on_room_message",function Q(b,c){var h=Q.CBS_root,d=unescape(c);try{var g=a.parseJSON(d)}catch(D){g={m:d}}d=h.striphtml(g.m).replace(/\s*/g,

"");return 0===d.indexOf(window.CBSv2_ping)?(h.sanitize(b)===h.room&&H(d),!0):Q.CBS_orig.call(this,b,c)});I("html_handler.message_inbound.on_room_message",function M(a,b,h){var c=M.CBS_root,d=c.striphtml(b.m).replace(/\s*/g,"");return 0===d.indexOf(window.CBSv2_ping)?((c.sanitize(a)||c.sanitize(b.user))===c.room&&H(d),!0):S.test(d)?!0:M.CBS_orig.call(this,a,b,h)});I("websocket_handler.message_inbound.on_room_message",function h(a){if(void 0===a.m)return!0;var b=h.CBS_root,c=b.striphtml(a.m).replace(/\s*/g,

"");return 0===c.indexOf(window.CBSv2_ping)?(b.sanitize(a.user)===b.room&&H(c),!0):S.test(c)?!0:h.CBS_orig.call(this,a)});I("ws_handler.message_inbound.on_room_message",function C(b,c){var h=C.CBS_root,d=c;try{var g=a.parseJSON(d)}catch(aa){g={m:d}}d=h.striphtml(g.m).replace(/\s*/g,"");return 0===d.indexOf(window.CBSv2_ping)?(h.sanitize(b)===h.room&&H(d),!0):C.CBS_orig.call(this,b,c)});b()&&(a('.info-user a[data-tab="apps_and_bots"]').unbind("click").click(function(){var b=a(".info-user div.apps_and_bots"),

c=a(".info-user .buttons a[data-tab='apps_and_bots']").attr("href");b.show();b.html(window.gettext("loading . . ."));N?b.load(c,N):b.load(c)}),N());if(!F){g.dialog("CBSv2Confirm",function(){return{build:function(){this.setting("defaultFocus","cancel")},prepare:function(){this.setHeader("<span style=\"color:#dc5500; font-family: 'UbuntuBold', Arial, Helvetica, sans-serif;\">"+R("fa fa-exclamation-triangle fa-2x")+"CBSv2: "+this.get("title")+"</span>")}}},!0,"confirm");var F=g.CBSv2Confirm}if(!T){g.dialog("CBSv2Alert",

function(){return{build:function(){this.setHeader("<span style=\"color:#dc5500; font-family: 'UbuntuBold', Arial, Helvetica, sans-serif;\">"+R("fa fa-cog fa-2x")+"CBSv2: CB app/bot data Save'n'restore bookmarklet v2</span>")},setup:function(){return{buttons:[{text:"Close",key:27}],focus:{element:0},options:{maximizable:!1,resizable:!1,padding:!1}}},prepare:function(){var b=a(".CBSv2_buttons button");"false"!==t.CBSv2_use_local_storage?b.filter(".lf").hide():(a("#CBSv2_as").prop("disabled",!0),b.filter(".ld, .rm, .da").hide());

a('.chat-box ul.buttons li a[data-tab="autosave"]').length&&b.filter(".sv, .ld, .lf").prop("disabled",!0);b.click(function(c){c.preventDefault();b.prop("disabled",!0);Z(a(this))});a("#CBSv2_ls").change(function(){var c=a('.chat-box ul.buttons li a[data-tab="autosave"]').length;t.CBSv2_use_local_storage=a(this).is(":checked");"false"!==t.CBSv2_use_local_storage?(a("#CBSv2_as").prop("disabled",!1),b.filter(".ld").prop("disabled",c),b.filter(".ld, .rm, .da").show(),b.filter(".lf").hide()):(a("#CBSv2_as").prop("disabled",

!0),b.filter(".ld, .rm, .da").hide(),b.filter(".lf").prop("disabled",c),b.filter(".lf").show(),a("#CBSv2_as").removeProp("checked").change())});a("#CBSv2_as").change(function(){a(this).is(":checked")?(a('.chat-box ul.buttons li a[data-tab="settings"]').parent().before(a("<li></li>").html('<a href="#" data-tab="autosave" class="nooverlay">AutoSave</a>')),b.filter(".sv, .ld, .lf").prop("disabled",!0)):(a('.chat-box ul.buttons li a[data-tab="autosave"]').parent().remove(),b.filter(".sv, .ld, .lf").prop("disabled",

!1))})},hooks:{onclose:function(){a('.chat-box ul.buttons li a[data-tab="autosave"]').length?(window.CBSv2_autosave_interval=setInterval(x,18E4),setTimeout(x,6E3)):window.CBSv2_autosave_interval=null;a(".CBSv2_buttons").remove();f()}}}},null,"alert");var T=g.CBSv2Alert}var G=(new Date).valueOf();m.enm(E,function(){var c;var d=m.some(function(a){return a});var g=b();v.append(P);O();if(r.length||d&&g){for(c=0;c<m.length;c++)if(m[c]&&g){var f=a('<button data-s="^" data-a="$" class="sv">Save ^ "$" data</button>'.replace(/\^/g,

B[c]).replace(/\$/g,m[c]));v.append(f);f=a('<button data-s="^" data-a="$" class="lf">Restore ^ "$" saved data from file</button>'.replace(/\^/g,B[c]).replace(/\$/g,m[c]));v.append(f);for(d=0;d<r.length;d++)if(m[c]===r[d][1]){var k=r[d][4];f=a(('<button data-s="^" data-i="'+d+'" data-k="'+k+'" class="ld">Restore "'+r[d][5]+'" saved data into ^</button>').replace(/\^/g,B[c]));v.append(f)}}for(d=0;d<r.length;d++)f=a('<button data-i="'+d+'" data-k="'+k+'" class="rm">Delete "'+r[d][5]+'" saved data</button>'),

v.append(f);1<r.length&&(f=a('<button class="da">Delete ALL saved data</button>'),v.append(f));f=a('<input type="checkbox" id="CBSv2_ls"><label for="CBSv2_ls">Use browser Local Storage</label>');"false"!==t.CBSv2_use_local_storage&&f.prop("checked",!0);v.append(f);g&&(f=a('<input type="checkbox" id="CBSv2_as"><label for="CBSv2_as">AutoSave</label>'),a('.chat-box ul.buttons li a[data-tab="autosave"]').length&&f.prop("checked",!0),v.append(f))}else v.append(a("<p>Nothing to do!</p>"));z=T(v[0])})});

The most convenient way of running the applet is to copy and paste it into your browser's Bookmark Toolbar, so that it is available as a bookmark applet (bookmarklet*) to click on whenever you want to open the Save and Restore Menu.
 
*A WORD OF CAUTION: It is not normally advisable to use bookmarklets from unverified sources, as they can be dangerous to the health of your computer. Only use bookmarklets from sources that you trust, especially when the javascript has been optimized, as the above has been, and it is not obvious what the applet does. The unoptimized code is provided below for you to verify. But if you are in any doubt, DO NOT use it.
 
Save'N'Restore Menu
Once the bookmarklet has been added to your browser, you can open the Save'N'Restore menu whenever you're on the site.
The menu options available will depend on whether you are in-chat, whether there are any CBS-enabled scripts running, and whether you have chosen to use browser Local Storage.
A checkbox at the foot of the menu allows you to choose to use browser Local Storage.
If browser Local Storage is selected, a further checkbox allows you to choose to enable AutoSave mode.
 
AutoSave Mode
While AutoSave is enabled, an unselectable AutoSave tab is displayed in the chat panel, and the CBS applet will attempt to save the data from any active CBS-enabled scripts automatically every three minutes and when you deactivate a script.
However, an AutoSave can fail if a script crashes, or the site becomes unresponsive.
In addition, AutoSave will be disabled if you refresh or reload the page. It can be re-enabled via the menu.
Finally, in order to Restore, or Re-load, previously saved data, AutoSave mode must be temporarily disabled.
It is important to appreciate that there is no equivalent "AutoRestore". Previously saved data can only be Restored or Re-loaded manually via the Save'N'Restore Menu, either from file or from Local Storage, and AutoSave mode must be temporarily unselected, in order to do that.
 
CBS Module
In order to make an app or bot CBS-aware the following module must be added to the head of the script:

// startof CBSv2.010 module - not for re-compilation

(function(a,k){function g(a){this.message=a}g.prototype=Error();g.prototype.name="InvalidCharacterError";a.btoa||(a.btoa=function(a){a=String(a);for(var f,b,n=0,c=k,q="";a.charAt(n|0)||(c="=",n%1);q+=c.charAt(63&f>>8-n%1*8)){b=a.charCodeAt(n+=.75);if(255<b)throw new g('"btoa" failed: The string to be encoded contains characters outside of the Latin1 range.');f=f<<8|b}return q});a.atob||(a.atob=function(a){a=String(a).replace(/=+$/,"");if(1==a.length%4)throw new g('"atob" failed: The string to be decoded is not correctly encoded.');

for(var f=0,b,n,c=0,q="";n=a.charAt(c++);~n&&(b=f%4?64*b+n:n,f++%4)?q+=String.fromCharCode(255&b>>(-2*f&6)):0)n=k.indexOf(n);return q})})("undefined"===typeof exports?this:exports,"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=");

(function(a){var k=cb.onMessage,g=null,u=null,f,b="#"+(cb.settings.hasOwnProperty("slot")?cb.settings.slot:"")+"CBSv2",n=/^\/#[0-3]CBSv2\//;cb.log("CBS::v2::CB app/bot data Save/restore::20171104.010::Release");cb.onMessage=function(c){if("function"!==typeof c)throw new TypeError(c+" is not a function");k(function(k){var d=k.m.replace(/\s*/g,"").split("/");if(3<d.length&&""===d[0]&&d[1]===b){if(g&&u&&k.user===cb.room_slug){var p=d[2];if(4===d.length)d[3]="?",k.m=d.join("/");else if(6===d.length){if(!(f||

{}).hasOwnProperty(p)){var q=g();f={};f[p]=a.btoa(a.unescape(a.encodeURIComponent(q)));q||cb.log("onSave returned no data.")}f.hasOwnProperty(p)&&(q=parseInt(d[3],10),p=f[p].slice(q,q+512),d[4]=p,d[5]=p.length,k.m=d.join("/"))}else 7===d.length&&("0"===d[3]&&(f={},f[p]=""),f.hasOwnProperty(p)&&(d[3]=f[p].length,d[6]=d[4].length,k.m=d.join("/"),d[4]?f[p]+=d[4]:(q=a.decodeURIComponent(a.escape(a.atob(f[p]))),u(q),cb.chatNotice("Previously Saved Data Restored.",cb.room_slug))))}k["X-Spam"]=!0}else n.test(k.m)&&

(k["X-Spam"]=!0);return c(k)});return c};cb.onRestore=function(a){if("function"!==typeof a)throw new TypeError(a+" is not a function");return u=a};cb.onSave=function(a){if("function"!==typeof a)throw new TypeError(a+" is not a function");return g=a};cb.onMessage(function(a){return a})})("undefined"===typeof exports?this:exports);

// endof CBSv2.010 module - not for re-compilation


In addition, in order to make an app or bot CBS-enabled, two new handler callback functions must be registered using cb.onSave(func); and cb.onRestore(func); statements.
Examples of both types of callback can be found in this app's source code.
 
Briefly, the CBS add-on saves and restores a piece of string data. So, if the app only needs to save a single string variable, the onSave callback just needs to return that string, and the onRestore callback just needs to set the variable to the value of its argument, which will be the previously saved string value.
 
The example callbacks in this app illustrate the more common situation, where more than a single piece of data is to be saved and restored. And, although in this demo all the app variables are saved and restored, exactly what pieces of data should be saved and restored is entirely a decision for the script author, as they feel is appropriate. For instance, in the CrazyTicket alternative, AutoStart Tip2View, only show ticket purchasers and unexpired preview ticket purchasers are saved and restored.
 
Using Save'N'Restore with Non-CBS-Aware Apps and Bots
The way that the Save'N'Restore bookmark applet works is that every time its menu is opened (and every time the AutoSave process runs, if it is enabled) each active app and bot is pinged with a message to test whether it is CBS-enabled. The applet makes sure that broadcasters won't see those message pings. However, if you're running cbscripts that aren't CBS-aware, that is those that don't include the CBS Save'N'Restore Module (which is most apps and bots), ping messages sent to those cbscripts will show up in the chat of visitors to your room. So you might get some people asking what the weird looking messages you keep posting are? They take the form of /#[slot number]CBSv[version number]/[time stamp]/ and are just the Save'N'Restore bookmarklet working behind the scenes, on your behalf ;)
 
Credits:


The bookmarklet menu system uses the excellent AlertifyJS library, created by Mohammad Younes, and the awesome FontAwesome iconic font and css toolkit, created by Dave Gandy.

The Base64 encoder/decoder module included in the CBS module is based on code originating from nignag and atk.


Also by the Same Author:


Too many to list. Just search for rubzombie


CBS Bookmarklet/Module Source Code:

The source code can be roughly divided into four sections:

a library, style, and code pre-loader,

the save/restorer controller,

the save/restorer cb object extender module, and

a Base64 encoder/decoder module.


// ==ClosureCompiler==

// @output_file_name default.js

// @compilation_level ADVANCED_OPTIMIZATIONS

// @externs_url https://raw.githubusercontent.com/google/closure-compiler/master/contrib/externs/jquery-1.6.js

// @js_externs var cbjs = { "arrayContains": function () {}, "arrayJoin": function () {}, "arrayRemove": function () {} }

// @js_externs var old_cb = { "changeRoomSubject": function () {}, "chatNotice": function () {}, "drawPanel": function () {}, "in_show": function () {}, "limitCam_addUsers": function () {}, "limitCam_allUsersWithAccess": function () {}, "limitCam_isRunning": function () {}, "limitCam_removeAllUsers": function () {}, "limitCam_removeUsers": function () {}, "limitCam_start": function () {}, "limitCam_stop": function () {}, "limitCam_userHasAccess": function () {}, "log": function () {}, "onDrawPanel": function () {}, "onEnter": function () {}, "onLeave": function () {}, "onMessage": function () {}, "onShowStatus": function () {}, "onTip": function () {}, "room_slug": {}, "sendNotice": function () {}, "setTimeout": function () {}, "settings": function () {}, "settings_choices": function () {}, "show_message": {}, "show_users": function () {}, "slot": function () {}, "tipOptions": function () {} }

// @js_externs var cb = { "app_id": {}, "cancelTimeout": function () {}, "changeRoomSubject": function () {}, "chatNotice": function () {}, "drawPanel": function () {}, "limitCam_addUsers": function () {}, "limitCam_allUsersWithAccess": function () {}, "limitCam_isRunning": function () {}, "limitCam_removeAllUsers": function () {}, "limitCam_removeUsers": function () {}, "limitCam_start": function () {}, "limitCam_stop": function () {}, "limitCam_userHasAccess": function () {}, "log": function () {}, "onDrawPanel": function () {}, "onEnter": function () {}, "onLeave": function () {}, "onMessage": function () {}, "onTip": function () {}, "panCam_controlsDisabled": {}, "panCam_controlsEnabled": {}, "panCam_isValidDirection": function () {}, "panCam_move": function () {}, "panCam_onPanelButtonClicked": function () {}, "room_slug": {}, "sendNotice": function () {}, "setTimeout": function () {}, "settings": { "slot": {} }, "settings_choices": {}, "slot": {}, "tipOptions": function () {} }

// ==/ClosureCompiler==

/*jslint ass: true, vars: true, sub: true, nomen: true, plusplus: true, regexp: true, indent: 2, white: true */

/*global window, document, console, alert, setInterval, clearInterval, setTimeout, clearTimeout, unescape, cb:false, exports:false */

/**

 * @author rubzombie

 */

const app = "CBS";

const sDesc = "CB app/bot data Save/restore";

const sVer = "20171104.010";

const debug = false;

const persist = true; // persist element additions, i.e. don't reload the jquery/alertifyjs/fontawesome libraries each time

const ver = "v2";

const sSig = app + "::" + ver + "::" + sDesc + "::" + sVer + (debug ? "::Debug" : "::Release");

const magicnumber = app + ver;

const id = magicnumber + "Overlay";

const chunk_size = 512;

const yield_delay = 1;

/*javascript:*//*** CB app/bot data Save'n'restore bookmarklet v2 ***/(function () {

  "use strict";

  // Enable the passage of the 'this' object through the JavaScript timers

  window["__nativeST__"] = window["__nativeST__"] || window["setTimeout"];

  window["__nativeSI__"] = window["__nativeSI__"] || window["setInterval"];

  window["setTimeout"] = function (vCallback, nDelay /*, argumentToPass1, argumentToPass2, etc. */ ) {

    var oThis = this,

      aArgs = Array.prototype.slice.call(arguments, 2);

    return window["__nativeST__"](vCallback instanceof Function ? function () {

      vCallback.apply(oThis, aArgs);

    } : vCallback, nDelay);

  };

  window["setInterval"] = function (vCallback, nDelay /*, argumentToPass1, argumentToPass2, etc. */ ) {

    var oThis = this,

      aArgs = Array.prototype.slice.call(arguments, 2);

    return window["__nativeSI__"](vCallback instanceof Function ? function () {

      vCallback.apply(oThis, aArgs);

    } : vCallback, nDelay);

  };

}());

(function (window, document, version, app_callback) {

  "use strict";

  window[app] = { "version": sSig };

  /*** jQuery/alertifyjs/font-awesome preloader v3, startof ***/

  version = version || "1";

  app_callback = app_callback || function (a, b, c, d, cont) {

    setTimeout(cont, 10000, a, b, c, d);

  };

  const root = "//cdn.jsdelivr.net/alertifyjs/1.4.1/";

  var element = document.getElementById(id), // NB. element is re-cycled after first use

    cleanup = [],

    $,

    $_loaded = false,

    ajs,

    ajs_loaded = false,

    alertify,

    state;

  //

  /**

  * @param {Array<Function>} tasks

  * @param {number=} idx

  */

  function run(tasks, idx) {

    idx = idx || 0;

    tasks[idx++]((function next(tasks, idx) {

      return function () { // NB. this closure may be unnecessary, but better safe than sorry

        if (idx < tasks.length) {

          window.setTimeout(function () {

            run(tasks, idx);

          }, yield_delay);

        }

      };

    }(tasks, idx)));

  }

  // retrict to *chaturbate.com and prevent multiple instances

  if (!/(camgasm|chaturbate)\.com$/i.test(document.location.hostname)) {

    window.alert("*chaturbate.com only bookmarklet");

  } else if (!element) {

    run([

      function (cont) {

        /**/

        // splash screen

        //var style = ("<style>." + magicnumber + "-no-overflow {overflow:hidden!important;outline:0;}${visibility:visible;position:fixed;top:0;left:0;height:100%;width:100%;z-index:10;background-color:rgba(0,0,0,0.05);}$ div {visibility:visible;width:300px;margin:100px auto;background-color:#fff;border:1px solid #000;padding:15px;text-align:center;}</style>").replace(/\$/g, "#" + id);

        var style = ("<style>." + magicnumber + "-no-overflow {overflow:hidden!important;outline:0;}${visibility:visible;position:fixed;top:0;right:0;left:0;bottom:0;overflow-y:auto;padding:0;z-index:10;background-color:rgba(0,0,0,0.05);}$ div {visibility:visible;width:300px;margin:100px auto;background-color:#fff;border:1px solid #000;padding:15px;text-align:center;}</style>").replace(/\$/g, "#" + id);

        //

        if (debug) {

          console.log(">do overlay and splash screen");

        }

        element = document.createElement("div");

        element.setAttribute("id", id);

        element.style.display = "block";

        element.innerHTML = style + "<div>Starting...</div>";

        document.body.appendChild(element);

        document.body.classList.add(magicnumber + "-no-overflow");

        cleanup.push(element);

        setTimeout(function () {

          cont();

        }, 1000);

      },

      function (cont) {

        if (debug) {

          console.log(">do jquery load");

        }

        $ = window.jQuery;

        if ($ && version <= $.fn.jquery) {

          cont();

        } else {

          element = document.createElement("script");

          element.type = "text/javascript";

          element.src = "//ajax.googleapis.com/ajax/libs/jquery/" + version + "/jquery.min.js";

          element.onload = element.onreadystatechange = function () {

            state = this.readyState;

            if (!$_loaded && (!state || state === "loaded" || state === "complete")) {

              ($ = window.jQuery).noConflict($_loaded = true);

              if (debug) {

                console.log("loaded: " + element.src);

              }

              // Handle memory leak in IE

              element.onload = element.onreadystatechange = null;

              cont();

            }

          };

          //document.head.appendChild(element);

          document.documentElement.childNodes[0].appendChild(element);

          if (!persist) {

            cleanup.push(element);

          }

        }

      },

      function (cont) {

        if (debug) {

          console.log(">do stylesheet load(s)");

        }

        // Loading style definitions

        [

          [root + "css/alertify.min.css", "alertify-notifier ajs-left", "left", "10px"],

          [root + "css/themes/default.min.css"], // note no adequate test for theme stylesheet, so just test for file presence

          ["//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css", "fa", "font-family", "FontAwesome"]

        ].forEach(function (val) {

          function testStyleRule(url, name, css, value) {

            var dummy = $("<div>").hide().css({

                height: 0,

                width: 0

              }).addClass(name || "").appendTo("body"),

              res = dummy.css(css || "") === value; // true if name css and value are all undefined

            //

            dummy.remove();

            return $("link[rel=\"stylesheet\"][href$=\"" + url.match(/(?:\/)[^\/]+$/) + "\"]").length && res;

          }

          if (!testStyleRule.apply(null, val)) {

            element = document.createElement("link");

            element.setAttribute("rel", "stylesheet");

            element.setAttribute("href", val[0]);

            document.head.appendChild(element);

            if (debug) {

              console.log("loaded: " + val[0]);

            }

            if (!persist) {

              cleanup.push(element);

            }

          }

        });

        cont();

      },

      function (cont) {

        if (debug) {

          console.log(">do alertifyjs load");

        }

        // check for alertifyjs else Load the script and when it's ready run the cont callback.

        ajs = window["alertify"];

        if (ajs && ajs["dialog"]) {

          cont();

        } else {

          // Load the script and when it's ready loading run the cont callback.

          // BEWARE. recycling element and state var

          element = document.createElement("script");

          element.src = root + "alertify.min.js";

          // Attach handlers for all browsers

          element.onload = element.onreadystatechange = function () {

            state = this.readyState;

            if (!ajs_loaded && (!state || state === "loaded" || state === "complete")) {

              alertify = window["alertify"];

              window["alertify"] = ajs;

              ajs = alertify;

              ajs_loaded = true;

              if (debug) {

                console.log("loaded: " + element.src);

              }

              // Handle memory leak in IE

              element.onload = element.onreadystatechange = null;

              cont();

            }

          };

          //document.head.appendChild(element);

          document.documentElement.childNodes[0].appendChild(element);

          if (!persist) {

            cleanup.push(element);

          }

        }

      },

      function (cont) {

        if (debug) {

          console.log(">clear splash screen, after 3s delay");

        }

        setTimeout(function () {

          var child = document.querySelector("#" + id + " div");

          if (child) {

            child.style.visibility = "hidden";

          }

        }, 3000);

        cont();

      },

      function (cont) {

        if (debug) {

          console.log(">run app");

        }

        app_callback($, $_loaded, ajs, ajs_loaded, cont);

      },

      function (cont) {

        if (debug) {

          console.log(">schedule alertifyjs remnants removal");

        }

        // delay alertify remnants removal, so as not to interfere with alertifyjs's own cleanup process

        if (ajs_loaded) {

          setTimeout(function () {

            Array.prototype.slice.call(document.querySelectorAll("div[class*=\"alertify-\"],div[class*=\"ajs-\"]")).forEach(function (node) {

              node.parentElement.removeChild(node);

            });

          }, 2000);

        }

        cont();

      },

      function (cont) {

        if (debug) {

          console.log(">do final cleanup, remove any added script and link tags");

        }

        // final cleanup

        cleanup.forEach(function (element) {

          element.parentNode.removeChild(element);

        });

        document.body.classList.remove(magicnumber + "-no-overflow");

        cont();

      }

    ]);

  }

  /*** jQuery/alertifyjs/font-awesome preloader v3, endof ***/

}(window, document, "1.6.4", function ($, $L, alertify, alertifyL, complete) {

  // CBS alertifyjs'd app/bot data combined save/restorer

  "use strict";

  if (debug) {

    console.log("jQuery loaded: ", $L, " AlertifyJS loaded: ", alertifyL);

  }

  const post_timeout_delay = 5 * 1000; // msec

  const autosave_interval_delay = (debug ? 1 : 3) * 60 * 1000; // msec

  const retry_max = 5;

  const retry_delay = 1000; // msec

  const notify_delay = 3; // sec

  var _alert,

    confirm,

    dialog,

    div = $("<div class=" + magicnumber + "_buttons></div>"),

    style = $("<style>.$_buttons{margin:0 auto;padding:10px 20px;}.$_buttons button{display:block;width:100%;margin:5px 0;}.$_buttons input[type=checkbox][disabled] + label{color: #ccc;}</style>".replace(/\$/g, magicnumber)),

    slots = ["Active App", "Bot #1", "Bot #2", "Bot #3"];

  /** @type {Object} */

  var apps = ["", "", "", ""];

  var ops = {

      4: "Query",

      6: "Save",

      7: "Restore"

    },

    ls = window["localStorage"],

    saves,

    timestamp,

    data,

    post_timeout = null,

    reTag = new RegExp("^\\/#[0-3]" + magicnumber + "\\/"); // NB. this must match reTag definition below

  //

  function inChat() {

    return ((window["ws_handler"] && window["ws_handler"]["connected"]) || (window["websocket_handler"] && window["websocket_handler"]["connected"]) || (window["flash_handler"] && window["flash_handler"]["connected"]) || (window["html_handler"] && window["html_handler"]["connected"]));

  }

  function ts() {

    var a = new Date(),

      b = /(..)(:..)(:..)/.exec(a),

      c = b[1] % 12 || 12; // NB.NB.NB. must manually insert space or convert to hex number format after ClosureCompiler optimization, as %12 will be interpreted as an escape character, in a bookmarklet! see "Uncaught SyntaxError: Unexpected token ILLEGAL"

    return " " + (10 > c ? "0" + c : c) + b[2] + b[3] + " " + (12 > b[1] ? "A" : "P") + "M " + a.getTime().toString().slice(-3);

  }

  window[magicnumber + "_autosave_interval"] = window[magicnumber + "_autosave_interval"] || null;

  if (window[magicnumber + "_autosave_interval"]) {

    window.clearInterval(window[magicnumber + "_autosave_interval"]); // temporarily suspend autosave while CBS menu open

    if (debug) {

      console.log(ts() + ": AutoSave suspended");

    }

  }

  window[magicnumber + "_ping"] = window[magicnumber + "_ping"] || null;

  //apps["enm"] = function (fn, cb) { // enumerate function

  /**

  * @this {Object}

  */

  Array.prototype["enm"] = Array.prototype["enm"] || function (fn, cb) { // enumerate function fn over an Array and call cb on completion

    this["idx"] = ++this["idx"] || 0;

    if (arguments.length) {

      this["fn"] = fn || null;

      this["cb"] = cb || null;

    }

    if (this["idx"] < this.length) {

      if (this["fn"]) {

        this["fn"]["call"](this);

      }

    } else {

      delete this["idx"];

      if (this["cb"]) {

        this["cb"]["call"](this);

      }

    }

  };

  function setDialog(content) {

    div.empty().append(style, $("<p>" + content + "</p>"));

    if (dialog && dialog["isOpen"]()) {

      dialog["setContent"](div[0])["set"]("closable", true);

    }

  }

  function closeDialog() {

    if (debug) {

      console.log(ts() + ": closeDialog", dialog, dialog["isOpen"]());

    }

    if (dialog && dialog["isOpen"]()) {

      dialog["close"]()["set"]("closable", true);

    }

  }

  alertify.set("notifier", "delay", notify_delay);

  //alertify.set("notifier", "callback", closeDialog); // NB. this doesn't work. see alertifyjs source code. only set position or delay. 'arse! so...

  function success(message) {

    alertify["success"](message)["callback"] = closeDialog;

  }

  function error(message) {

    alertify["error"](message)["callback"] = closeDialog;

  }

  /*function notify(message) {

    alertify["notify"](message)["callback"] = closeDialog;

  }*/

  function postChat(message) {

    if (debug) {

      console.log(ts() + "> " + JSON.stringify(window[magicnumber + "_ping"]) + " | " + JSON.stringify(message));

    }

    var op = ops[window[magicnumber + "_ping"].split("/").length],

      handler;

    window["$message_sender"]["confirmed_send"] = true; // avoid first-post hurdle

    // @ref *_handler.message_outbound.send_room_message

    if ((handler = window["flash_handler"]) !== undefined && handler["connected"]) {

      handler["consolelog"]("User is sending message: " + message);

      message = $.toJSON({

        "m": message,

        "c": "",

        "f": ""

      });

      handler["consolelog"](message);

      window["GetFlashObject"]("movie")["SendRoomMsg"](message);

    } else if ((handler = window["html_handler"]) !== undefined && handler["connected"]) {

      handler["consolelog"]("User is sending message: " + message);

      message = $.toJSON({

        "m": message,

        "c": "",

        "f": ""

      });

      handler["consolelog"](message);

      // either

      //handler["post_html_chat"](message);

      // or, this if we don't want any spurious alert popups alarming the user ;)

      $.ajax({

        "url": handler["post_address"],

        "dataType": "json",

        "data": {

          "room": handler["room_owner_nick"],

          "message": message,

          "username": handler["user"]

        },

        "type": "POST",

        "success": function (response) {

          if (response === "") {

            if (debug) {

              error("An error occurred");

            }

            return;

          }

          if (response["X-Spam"]) {

            handler["message_inbound"]["on_room_message"](handler["user"], message);

          }

        }

      });

    } else if ((handler = window["websocket_handler"]) !== undefined && handler["connected"]) {

      handler["connection"]["send"](JSON.stringify({

        'action': 'msg',

        'msg': message

      }));

    } else if ((handler = window["ws_handler"]) !== undefined && handler["connected"]) {

      handler["consolelog"]("User is sending message: " + message);

      var message = $.toJSON({

        "m": message,

        "c": "",

        "f": ""

      });

      handler["consolelog"](message);

      handler["SendRoomMsg"](message);

    } else {

      if (debug) {

        console.log(ts() + ": \"" + op + "\" Failed. Not connected.");

      }

      if (window[magicnumber + "_autosave_interval"]) {

        apps["enm"]();

      } else {

        setDialog(op + " Failed. Chat may be disconnected.");

      }

    }

    //} // else if no handler, no message will be posted and ping will timeout

  }

  function qryApps() {

    var self = this;

    var $app = $(".stop_link[name=\"/app/stop/" + self["idx"] + "/\"]");

    self[self["idx"]] = $app.length ? $app[0].parentNode.previousElementSibling.innerHTML.trim() : "";

    if (self[self["idx"]].length && inChat()) {

      window[magicnumber + "_ping"] = ["", "#" + self["idx"] + magicnumber, timestamp, ""].join("/");

      postChat(window[magicnumber + "_ping"]);

      post_timeout = setTimeout.call(self, function () {

        // if ?? qry times out, app is most likely not CBS aware, so blank it and move on

        this[this["idx"]] = "";

        this["enm"]();

      }, post_timeout_delay);

    } else {

      self["enm"]();

    }

  }

  function onPostTimeout() {

    var op = ops[window[magicnumber + "_ping"].split("/").length];

    post_timeout = null;

    if (debug) {

      console.log(ts() + ": \"" + op + "\" Failed. Post timed-out."); // ie. the posted ping did not show up in chat yet

    }

    if (window[magicnumber + "_autosave_interval"]) {

      apps["enm"]();

    } else {

      setDialog(op + " Failed. Chat may be unresponsive.");

    }

  }

  /**

  * @param {number} slot

  * @param {number=} start

  */

  function txData(slot, start) {

    var chunk;

    start = start || 0;

    chunk = data.slice(start, start + chunk_size);

    window[magicnumber + "_ping"] = ["", "#" + slot + magicnumber, timestamp, start, chunk, chunk.length, ""].join("/");

    postChat(window[magicnumber + "_ping"]);

    post_timeout = setTimeout(onPostTimeout, post_timeout_delay);

  }

  /**

  * @param {number} slot

  * @param {number=} start

  */

  function rxData(slot, start) {

    start = start || 0;

    window[magicnumber + "_ping"] = ["", "#" + slot + magicnumber, timestamp, start, ""].join("/");

    postChat([window[magicnumber + "_ping"], ""].join("/"));

    post_timeout = setTimeout(onPostTimeout, post_timeout_delay);

  }

  function autosaveApps() {

    var self = this;

    if (self[self["idx"]].length && inChat() && window[magicnumber + "_autosave_interval"]) {

      if (debug) {

        console.log(ts() + ": " + "AutoSave \"" + slots[self["idx"]] + "\"");

      }

      rxData(self["idx"]);

    } else {

      self["enm"]();

    }

  }

  /**

  * @param {Function=} cb

  */

  function onAutoSave(cb) {

    function onAutoSaved() {

      if (debug) {

        console.log(ts() + ": AutoSave: onAutoSaved: retry: " + onAutoSave.retry);

      }

      if (!onAutoSave.saving) {

        cb.call(this);

      } else if (--onAutoSave.retry) {

        setTimeout(onAutoSaved, retry_delay);

      } else {

        if (debug) {

          console.log(ts() + ": AutoSave: onAutoSaved: Failed. retry timed-out.");

        }

        cb.call(this);

      }

    }

    if (window[magicnumber + "_autosave_interval"]) {

      if (!onAutoSave.saving) {

        onAutoSave.saving = true;

        if (!$("#" + id).length) { // don't autosave while overlay exists, ie. dialog is open

          if (debug) {

            console.log(ts() + ": AutoSave: " + JSON.stringify(apps));

          }

          apps["enm"](qryApps, function () {

            timestamp = (new Date()).valueOf();

            apps["enm"](autosaveApps, function () {

              onAutoSave.saving = false;

              if (cb) {

                cb.call(this);

              }

            });

          });

        } else { // shouldn't be reachable, as onAutoSave shouldn't be being called while menu is up

          if (debug) {

            console.log(ts() + "! Missed AutoSave. CBS Menu open.");

          }

        }

      } else if (cb) { // only need to wait if there is a callback

        onAutoSave.retry = retry_max;

        setTimeout(onAutoSaved, retry_delay);

      }

    } else if (cb) {

      cb.call(this);

    }

  }

  function getSaves() {

    var a, i;

    //

    saves = [];

    for (i = 0; i < ls.length; i++) {

      a = ls.key(i).split("/");

      while (a.length > 4) { // deal with appnames that include '/'

        a[1] = [a[1]].concat(a.splice(2, 1)).join("/");

      }

      if (a.length === 4 && a[0] === magicnumber/* && apps.indexOf(a[1]) !== -1*/) {

        saves.push(a.concat(ls.key(i), [

          a[1], // appname

          (new Date(parseInt(a[3], 10))).toLocaleString(), // timestamp

          slots[parseInt(a[2], 10)] // slot

        ].join(" ")));

      }

    }

  }

  //

  // v2.1

  // obj.m transformations

  // Qr /[_tag]/[timestamp]/ => /[_tag]/[timestamp]/?

  // a.length === 4 && a[3] === "" => a.length === 4 && a[3] !== ""

  // Sv /[_tag]/[timestamp]/[start]// => /[_tag]/[timestamp]/[start]/[sent]/[sent.length]

  // a.length === 6 && a[5] === "" => a.length === 6 && a[5] !== ""

  // Ld /[_tag]/[timestamp]/[start]/[sent]/[sent.length]/ => /[_tag]/[timestamp]/[start]/[sent]/[sent.length]/[received.length]

  // a.length === 7 && a[6] === "" => a.length === 7 && a[6] !== ""

  //

  function onRestore(msg_hdr, msg_tag, msg_timestamp, msg_data_start, msg_data, msg_data_length, msg_data_received_length) {

    if (debug) {

      console.log(ts() + ": " + "onRestore: hdr, ts: ", msg_hdr, msg_timestamp);

    }

    var slot = parseInt(msg_tag[1], 10);

    if (debug) {

      console.log(ts() + ": onRestore: ", msg_data.length, msg_data_length, msg_data_received_length);

    }

    if (msg_data) {

      txData(slot, parseInt(msg_data_start, 10) + parseInt(msg_data_received_length, 10));

    } else {

      //success("Restored."); // let CBS enabled app/bot report success

      closeDialog();

    }

  }

  function destroyClickedElement(event) {

    document.body.removeChild(event.target);

  }

  function saveAsFile(key, data) {

    var blob = new window["Blob"]([JSON.stringify({

      "key": key,

      "value": data

    })], {

      "type": "application/json"

    });

    var fileName = key.split("/").slice(0, -1).join("_") + ".json";

    var downloadLink = document.createElement("a");

    downloadLink.download = fileName;

    downloadLink.innerHTML = "Download File";

    if (window.webkitURL !== null) {

      // Chrome allows the link to be clicked

      // without actually adding it to the DOM.

      downloadLink.href = window.webkitURL.createObjectURL(blob);

    } else {

      // Firefox requires the link to be added to the DOM

      // before it can be clicked.

      downloadLink.href = window.URL.createObjectURL(blob);

      downloadLink.onclick = destroyClickedElement;

      downloadLink.style.display = "none";

      document.body.appendChild(downloadLink);

    }

    downloadLink.click();

  }

  function loadFromFile(target_slot) {

    var fileInput = document.createElement("input");

    fileInput.type = "file";

    fileInput.onchange = function (event) {

      var file = event.target.files[0];

      var reader = new window["FileReader"]();

      reader.onload = function (fileLoadedEvent) {

        var loaded = JSON["parse"](fileLoadedEvent.target.result);

        var a = (loaded["key"] || "").split("/");

        while (a.length > 4) { // deal with appnames that include '/'

          a[1] = [a[1]].concat(a.splice(2, 1)).join("/");

        }

        if (a.length === 4 && a[0] === magicnumber && a[1] === apps[target_slot]) {

          data = loaded["value"];

          txData(target_slot);

        } else {

          //setDialog("\"" + file["name"] + "\" does not contain \"" + apps[target_slot] + "\" save data."); // can't use dialog, cos it's closed by now

          alert("\"" + file["name"] + "\" does not contain \"" + apps[target_slot] + "\" save data.");

        }

      };

      reader.readAsText(file, "UTF-8");

      if (event.target.parentNode) {

        event.target.parentNode.removeChild(event.target);

      }

    };

    if (window.webkitURL !== null) { // yeah, it's an empty block, jslint. so what? it'll get compiled out.

      // Chrome allows the elements to be clicked

      // without actually adding them to the DOM.

    } else {

      // Firefox requires them to be added to the DOM

      // before they can be clicked.

      fileInput.style.display = "none";

      document.body.appendChild(fileInput); // potential memory leak if user cancels file open dialog

    }

    fileInput.click();

  }

  function onSave(msg_hdr, msg_tag, msg_timestamp, msg_data_start, msg_data, msg_data_length) {

    if (debug) {

      console.log(ts() + ": " + "onSave: hdr, ts: ", msg_hdr, msg_timestamp);

    }

    var i,

      slot = parseInt(msg_tag[1], 10),

      found = -1;

    function okSave() {

      var key = [magicnumber, apps[slot], slot, msg_timestamp].join("/"); // magicnumber/appname/slot/timestamp

      if (debug) {

        console.log(ts() + ": onSave: " + (window[magicnumber + "_autosave_interval"] ? "Auto" : "") + "Saved.");

      }

      if (ls[magicnumber + "_use_local_storage"] !== "false") {

        ls[key] = data;

        if (window[magicnumber + "_autosave_interval"]) {

          apps["enm"]();

        } else {

          success("Saved.");

        }

      } else {

        saveAsFile(key, data);

        closeDialog();

      }

    }

    function okOverwrite() {

      ls["removeItem"](saves[found][4]);

      okSave();

    }

    if (debug) {

      console.log(ts() + ": onSave: ", msg_data.length, parseInt(msg_data_length, 10));

    }

    if (msg_data_start === "0") {

      data = msg_data;

    } else {

      data += msg_data;

    }

    if (msg_data) {

      rxData(slot, data.length);

    } else {

      if (data.length) {

        if (ls[magicnumber + "_use_local_storage"] !== "false") {

          if (window[magicnumber + "_autosave_interval"]) {

            getSaves();

          }

          for (i = 0; i < saves.length; i++) {

            if (msg_tag[1] === saves[i][2] && apps[slot] === saves[i][1]) {

              found = i;

              break;

            }

          }

          if (found > -1) {

            if (window[magicnumber + "_autosave_interval"]) {

              okOverwrite();

            } else {

              confirm("Confirm Overwrite...", "Overwrite existing \"" + saves[found][5] + "\" saved data?", okOverwrite, function () {

                if (debug) {

                  console.log(ts() + ": Overwrite cancelled.");

                }

                error("Overwrite cancelled.");

              });

            }

          } else {

            okSave();

          }

        } else {

          okSave();

        }

      } else {

        if (debug) {

          console.log(ts() + ": onSave: null data");

        }

        if (window[magicnumber + "_autosave_interval"]) {

          apps["enm"]();

        } else {

          setDialog("No Data. \"" + apps[slot] + "\" did not respond with data to save.");

        }

      }

    }

  }

  function onData(message) {

    if (post_timeout) {

      clearTimeout(post_timeout);

      post_timeout = null;

    }

    if (debug) {

      console.log(ts() + ": onData: ", JSON.stringify(message));

    }

    var op = ops[window[magicnumber + "_ping"].split("/").length],

      a = message.split("/"),

      slot = parseInt(a[1][1], 10);

    //

    if (a[a.length - 1] === "") {

      if (a.length === 4) { // no response to Query means app is not CBS aware, so ignore it and move on

        // either

        apps[slot] = "";

        // or

        //apps[apps["idx"]] = ""; // in this case slot === apps[apps.idx]

        apps["enm"]();

      } else { // no response from any other ping type is an error

        if (debug) {

          console.log(ts() + ": \"" + op + "\" Failed. No Response");

        }

        setDialog(op + " Failed. \"" + apps[slot] + "\" did not respond to \"" + op + "\" command.");

      }

    } else if (a.length === 4) {

      // Qr

      apps["enm"](); // note apps.enm knows all about continue callback

    } else if (a.length === 6) {

      // Sv

      onSave.apply(this, a);

    } else if (a.length === 7) {

      // Ld

      onRestore.apply(this, a);

    } else {

      if (debug) {

        console.log(ts() + ": Houston, we have a problem!"); // 'when the myth becomes legend, print the myth'

      }

      if (window[magicnumber + "_autosave_interval"]) {

        if (debug) {

          console.log(ts() + ": \"" + apps[slot] + "\" gave an unknown response to \"AutoSave\" command.");

        }

        apps["enm"]();

      } else {

        setDialog(op + " Aborted. \"" + apps[slot] + "\" gave an unknown response to \"" + op + "\" command.");

      }

    }

  }

  function handle($btn) {

    var i;

    function cancelDeleteALL() {

      if (debug) {

        console.log(ts() + ": " + "Delete ALL cancelled.");

      }

      error("Delete ALL cancelled.");

    }

    dialog["set"]("closable", false);

    $btn.html("Processing...");

    switch ($btn.attr("class")) {

    case "da":

      if (debug) {

        console.log(ts() + ": " + "Delete ALL");

      }

      confirm("Confirm Delete ALL...", "Delete ALL saved data?", function () {

        confirm("Confirm Confirm Delete ALL...", "Are you really sure you want to Delete ALL saved data?", function () {

          for (i = 0; i < saves.length; i++) {

            ls["removeItem"](saves[i][4]);

          }

          success("ALL Deleted.");

        }, cancelDeleteALL);

      }, cancelDeleteALL);

      break;

    case "ld":

      if (debug) {

        console.log(ts() + ": Restore \"" + $btn.data("k") + "\" to \"" + $btn.data("s") + "\"");

      }

      data = ls[saves[$btn.data("i")][4]];

      txData(slots.indexOf($btn.data("s")));

      break;

    case "lf":

      if (debug) {

        console.log(ts() + ": Restore file to \"" + $btn.data("s") + "\"");

      }

      loadFromFile(slots.indexOf($btn.data("s")));

      closeDialog();

      break;

    case "rm":

      if (debug) {

        console.log(ts() + ": Delete \"" + $btn.data("k") + "\"");

      }

      confirm("Confirm Delete...", "Delete \"" + saves[$btn.data("i")][5] + "\" saved data?", function () {

        ls["removeItem"]($btn.data("k"));

        success("Deleted.");

      }, function () {

        if (debug) {

          console.log(ts() + ": Delete cancelled.");

        }

        error("Delete cancelled.");

      });

      break;

    case "sv":

      if (debug) {

        console.log(ts() + ": Save \"" + $btn.data("s") + "\"");

      }

      rxData(slots.indexOf($btn.data("s")));

      break;

    }

  }

  //function subclass(ns_string, func) {

    //function leaf(obj, ns_string) {

      //var parts = ns_string.split("."),

        //i,

        //length = parts.length;

      //for (i = 0; i < length; i++) {

        //if (obj === undefined || !obj.hasOwnProperty(parts[i])) {

          //return null;

        //}

        //obj = obj[parts[i]];

      //}

      //return obj;

    //}

    //var chain = ns_string.split("."),

      //target = chain[chain.length - 1],

      //handler = leaf(window, chain[0]),

      //node = leaf(window, chain.slice(0, -1).join("."));

    //if (handler && node && node.hasOwnProperty(target)) {

      //if (!node.hasOwnProperty("orig_" + target)) {

        //node["orig_" + target] = node[target];

      //}

      //node[target] = func;

    //}

  //}

  const _orig = "_orig";

  const _handler = "_root";

  function subclass2(ns_string, func) {

    function leaf(obj, ns_string) {

      var parts = ns_string.split("."),

        i,

        length = parts.length;

      for (i = 0; i < length; i++) {

        if (obj === undefined || !obj.hasOwnProperty(parts[i])) {

          return null;

        }

        obj = obj[parts[i]];

      }

      return obj;

    }

    var chain = ns_string.split("."),

      target = chain[chain.length - 1],

      handler = leaf(window, chain[0]),

      node = leaf(window, chain.slice(0, -1).join("."));

    if (handler && node && node.hasOwnProperty(target) && typeof node[target] === "function") {

      var orig = node[target][app + _orig];

      if (typeof orig !== "function") {

        orig = node[target];

      }

      node[target] = func;

      node[target][app + _orig] = orig;

      node[target][app + _handler] = handler;

    } else if (debug) {

      console.error("failed to subclass: ", ns_string);

    }

  }

  subclass2("flash_handler.message_inbound.on_room_message", function fn(nick, message) {

    var handler = fn[app + _handler],

      m = unescape(message),

      msginfo;

    try {

      msginfo = $.parseJSON(m);

    } catch (e) {

      if (debug) {

        console.error(e.message);

      }

      msginfo = {

        "m": m

      };

    }

    m = handler["striphtml"](msginfo["m"]).replace(/\s*/g, ""); // must strip space cos cb tends to add it to messages

    if (debug) {

      console.log(ts() + "< " + JSON.stringify(window[magicnumber + "_ping"]) + " | " + JSON.stringify(m));

    }

    if (m.indexOf(window[magicnumber + "_ping"]) === 0) {

      if (handler["sanitize"](nick) === handler["room"]) {

        onData(m);

      }

      return true;

    }

    return fn[app + _orig].call(this, nick, message);

  });

  subclass2("html_handler.message_inbound.on_room_message", function fn(nick, msginfo, index) {

    var handler = fn[app + _handler],

      m = handler["striphtml"](msginfo["m"]).replace(/\s*/g, ""); // must strip space cos cb tends to add it to messages

    if (m.indexOf(window[magicnumber + "_ping"]) === 0) {

      if (debug) {

        console.log(ts() + "< " + JSON.stringify(window[magicnumber + "_ping"]) + " | " + JSON.stringify(m));

      }

      if ((handler["sanitize"](nick) || handler["sanitize"](msginfo["user"])) === handler["room"]) {

        onData(m);

      }

      return true;

    }

    if (reTag.test(m)) {

      if (debug) {

        console.log(ts() + "? " + JSON.stringify(window[magicnumber + "_ping"]) + " | " + JSON.stringify(m));

      }

      return true;

    }

    if (debug) {

      console.log(ts() + "< " + JSON.stringify(m));

    }

    return fn[app + _orig].call(this, nick, msginfo, index);

  });

  subclass2("websocket_handler.message_inbound.on_room_message", function fn(msginfo) {

    if (msginfo["m"] === undefined) {

      return true;

    }

    var handler = fn[app + _handler],

      m = handler["striphtml"](msginfo["m"]).replace(/\s*/g, ""); // must strip space cos cb tends to add it to messages

    if (m.indexOf(window[magicnumber + "_ping"]) === 0) {

      if (debug) {

        console.log(ts() + "< " + JSON.stringify(window[magicnumber + "_ping"]) + " | " + JSON.stringify(m));

      }

      if (handler["sanitize"](msginfo["user"]) === handler["room"]) {

        onData(m);

      }

      return true;

    }

    if (reTag.test(m)) {

      if (debug) {

        console.log(ts() + "? " + JSON.stringify(window[magicnumber + "_ping"]) + " | " + JSON.stringify(m));

      }

      return true;

    }

    if (debug) {

      console.log(ts() + "< " + JSON.stringify(m));

    }

    return fn[app + _orig].call(this, msginfo);

  });

  subclass2("ws_handler.message_inbound.on_room_message", function fn(nick, message) {

    var handler = fn[app + _handler],

      m = /*unescape*/(message),

      msginfo;

    try {

      msginfo = $.parseJSON(m);

    } catch (e) {

      if (debug) {

        console.error(e.message);

      }

      msginfo = {

        "m": m

      };

    }

    m = handler["striphtml"](msginfo["m"]).replace(/\s*/g, ""); // must strip space cos cb tends to add it to messages

    if (debug) {

      console.log(ts() + "< " + JSON.stringify(window[magicnumber + "_ping"]) + " | " + JSON.stringify(m));

    }

    if (m.indexOf(window[magicnumber + "_ping"]) === 0) {

      if (handler["sanitize"](nick) === handler["room"]) {

        onData(m);

      }

      return true;

    }

    return fn[app + _orig].call(this, nick, message);

  });

  if (debug) {

    subclass2("jQuery.mydefchatconn", function fn(method) {

      if (method === "app_tab_refresh" || method === "update_panel") {

        console.log("$.mydefchatconn(\"" + method + "\")");

      }

      return fn[app + _orig].apply(this, arguments);

    });

  }

  function setStopLink() {

    $(".stop_link").unbind("click").click(function () {

      var that = this;

      onAutoSave(function () {

        $.ajax({

          url: $(that).attr('name'),

          dataType: 'text',

          data: '',

          type: 'POST',

          success: function (/*response*/) {

            $["mydefchatconn"]('app_tab_refresh');

          }

        });

      });

    });

  }

  if (inChat()) {

    $('.info-user a[data-tab="apps_and_bots"]').unbind("click").click(function () {

      (function (tab, target, func) {

        tab.show();

        tab.html(window["gettext"]("loading . . ."));

        if (func) {

          tab.load(target, func);

        } else {

          tab.load(target);

        }

      }($(".info-user div.apps_and_bots"), $(".info-user .buttons a[data-tab='apps_and_bots']").attr('href'), setStopLink));

    });

    setStopLink();

  }

  var headerSpan = "<span style=\"color:#dc5500; font-family: 'UbuntuBold', Arial, Helvetica, sans-serif;\">";

  function af(icon) {

    return "<i class=\"" + icon + "\" style=\"vertical-align:middle; margin-right:20px;\"></i>";

  }

  // custom 'confirm' dialog

  if (!confirm) {

    alertify["dialog"](magicnumber + "Confirm", function () {

      return {

        "build": function () {

          this["setting"]("defaultFocus", "cancel");

        },

        "prepare": function () {

          this["setHeader"](headerSpan + af("fa fa-exclamation-triangle fa-2x") + magicnumber + ": " + this["get"]("title") + "</span>");

        }

      };

    }, true, "confirm");

    confirm = alertify[magicnumber + "Confirm"];

  }

  // custom 'alert' dialog

  if (!_alert) {

    alertify["dialog"](magicnumber + "Alert", function () {

      return {

        "build": function () {

          this["setHeader"](headerSpan + af("fa fa-cog fa-2x") + magicnumber + ": CB app/bot data Save'n'restore bookmarklet " + ver + "</span>");

        },

        "setup": function () {

          return {

            "buttons": [{

              "text": "Close",

              "key": 27 /*Esc*/

            }],

            "focus": {

              "element": 0

            },

            "options": {

              "maximizable": false,

              "resizable": false,

              "padding": false

            }

          };

        },

        "prepare": function () {

          var $btns = $("." + magicnumber + "_buttons button");

          if (ls[magicnumber + "_use_local_storage"] !== "false") {

            $btns.filter(".lf").hide();

          } else {

            $("#" + magicnumber + "_as").prop("disabled", true);

            $btns.filter(".ld, .rm, .da").hide();

          }

          if ($(".chat-box ul.buttons li a[data-tab=\"autosave\"]").length) {

            $btns.filter(".sv, .ld, .lf").prop("disabled", true);

          }

          $btns.click(function (evt) {

            evt.preventDefault();

            // either

            //$btns.attr("disabled", true);

            // or

            $btns.prop("disabled", true);

            handle($(this));

          });

          $("#" + magicnumber + "_ls").change(function (/*evt*/) {

            var isAutoSave = $(".chat-box ul.buttons li a[data-tab=\"autosave\"]").length;

            ls[magicnumber + "_use_local_storage"] = $(this).is(':checked'); //!(ls[magicnumber + "_use_local_storage"] === "true");

            if (ls[magicnumber + "_use_local_storage"] !== "false") {

              $("#" + magicnumber + "_as").prop("disabled", false);

              $btns.filter(".ld").prop("disabled", isAutoSave);

              $btns.filter(".ld, .rm, .da").show();

              $btns.filter(".lf").hide();

            } else {

              $("#" + magicnumber + "_as").prop("disabled", true);

              $btns.filter(".ld, .rm, .da").hide();

              $btns.filter(".lf").prop("disabled", isAutoSave);

              $btns.filter(".lf").show();

              $("#" + magicnumber + "_as").removeProp("checked").change();

            }

          });

          $("#" + magicnumber + "_as").change(function (/*evt*/) {

            if ($(this).is(':checked')) {

              if (debug) {

                console.log(ts() + ": AutoSave On");

              }

              $(".chat-box ul.buttons li a[data-tab=\"settings\"]").parent().before($("<li></li>").html("<a href=\"#\" data-tab=\"autosave\" class=\"nooverlay\">AutoSave</a>"));

              $btns.filter(".sv, .ld, .lf").prop("disabled", true);

              // see dialog onclose hooks for actual autosave init

            } else {

              if (debug) {

                console.log(ts() + ": AutoSave Off");

              }

              $(".chat-box ul.buttons li a[data-tab=\"autosave\"]").parent().remove();

              $btns.filter(".sv, .ld, .lf").prop("disabled", false);

              /*if (window[magicnumber + "_autosave_interval"]) {

                clearInterval(window[magicnumber + "_autosave_interval"]);

              }

              window[magicnumber + "_autosave_interval"] = null;*/

            }

          });

        },

        "hooks": {

          "onclose": function () {

            // autosave

            if ($(".chat-box ul.buttons li a[data-tab=\"autosave\"]").length) {

              window[magicnumber + "_autosave_interval"] = setInterval(onAutoSave, autosave_interval_delay);

              if (debug) {

                console.log(ts() + ": AutoSave activated");

              }

              setTimeout(onAutoSave, notify_delay * 2 * 1000); // schedule an ~immediate autosave - won't work if menu is still open. fingers crossed!

            } else {

              window[magicnumber + "_autosave_interval"] = null;

            }

            // clear menu

            $("." + magicnumber + "_buttons").remove();

            complete();

          }

        }

      };

    }, null, "alert");

    _alert = alertify[magicnumber + "Alert"];

  }

  function menu() {

    var i, j, k, btn,

      bSomeCBSApps = apps.some(function (v) {

        return v;

      }),

      bInChat = inChat();

    if (debug) {

      console.log(ts() + ": apps: " + JSON.stringify(apps));

    }

    div.append(style);

    getSaves();

    if (saves.length || (bSomeCBSApps && bInChat)) {

      // NB. while autosave is enabled, manual saves/restores are disabled

      for (j = 0; j < apps.length; j++) {

        if (apps[j] && bInChat) {

          btn = $(("<button data-s=\"^\" data-a=\"$\" class=\"sv\">Save ^ \"$\" data</button>").replace(/\^/g, slots[j]).replace(/\$/g, apps[j]));

          div.append(btn);

          btn = $(("<button data-s=\"^\" data-a=\"$\" class=\"lf\">Restore ^ \"$\" saved data from file</button>").replace(/\^/g, slots[j]).replace(/\$/g, apps[j]));

          div.append(btn);

          for (i = 0; i < saves.length; i++) {

            if (apps[j] === saves[i][1]) {

              k = saves[i][4]; // key

              btn = $(("<button data-s=\"^\" data-i=\"" + i + "\" data-k=\"" + k + "\" class=\"ld\">Restore \"" + saves[i][5] + "\" saved data into ^</button>").replace(/\^/g, slots[j]));

              div.append(btn);

            }

          }

        }

      }

      for (i = 0; i < saves.length; i++) {

        btn = $("<button data-i=\"" + i + "\" data-k=\"" + k + "\" class=\"rm\">Delete \"" + saves[i][5] + "\" saved data</button>");

        div.append(btn);

      }

      if (saves.length > 1) {

        btn = $("<button class=\"da\">Delete ALL saved data</button>");

        div.append(btn);

      }

      btn = $("<input type=\"checkbox\" id=\"" + magicnumber + "_ls\"><label for=\"" + magicnumber + "_ls\">Use browser Local Storage</label>");

      if (ls[magicnumber + "_use_local_storage"] !== "false") {

        btn.prop("checked", true);

      }

      div.append(btn);

      if (/*bSomeCBSApps && */bInChat) {

        btn = $("<input type=\"checkbox\" id=\"" + magicnumber + "_as\"><label for=\"" + magicnumber + "_as\">AutoSave</label>");

        if ($(".chat-box ul.buttons li a[data-tab=\"autosave\"]").length) {

          btn.prop("checked", true);

        }

        div.append(btn);

      }

    } else {

      // alert: nothing to do

      div.append($("<p>Nothing to do!</p>"));

      if (debug) {

        console.log(ts() + ": " + JSON.stringify({ "saves.length": saves.length, "bSomeCBSApps": bSomeCBSApps, "bInChat": bInChat}));

      }

    }

    dialog = _alert(div[0]);

  }

  timestamp = (new Date()).valueOf();

  apps["enm"](qryApps, menu); // once all active app/bots have been Query'd, create menu dialog to prompt for further action

}));

(function (exports) {

  "use strict";

  // startof CBS module

  var _cb_onMessage = cb["onMessage"],

    _onSave = null,

    _onRestore = null,

    _data, // object with single property: session timestamp

    _tag = "#" + (cb["settings"].hasOwnProperty("slot") ? cb["settings"]["slot"] : "") + magicnumber, // #[slot][magicnumber]

    reTag = new RegExp("^\\/#[0-3]" + magicnumber + "\\/"); // NB. this must match reTag definition above

  //

  // v2.1

  // obj.m transformations

  // Qr /[_tag]/[timestamp]/ => /[_tag]/[timestamp]/?

  // a.length === 4 && a[3] === "" => a.length === 4 && a[3] !== ""

  // Sv /[_tag]/[timestamp]/[start]// => /[_tag]/[timestamp]/[start]/[sent]/[sent.length]

  // a.length === 6 && a[5] === "" => a.length === 6 && a[5] !== ""

  // Ld /[_tag]/[timestamp]/[start]/[sent]/[sent.length]/ => /[_tag]/[timestamp]/[start]/[sent]/[sent.length]/[received.length]

  // a.length === 7 && a[6] === "" => a.length === 7 && a[6] !== ""

  //

  function message(obj) {

    var match = obj["m"].replace(/\s*/g, "").split("/"), // whitespace removal should be unnecessary. do it anyway

      timestamp,

      start,

      chunk,

      raw;

    //

    if (match.length > 3 && match[0] === "" && match[1] === _tag) {

      if (_onSave && _onRestore && obj["user"] === cb["room_slug"]) {

        if (debug) {

          cb["log"](obj["user"] + ": " + obj["m"]);

        }

        timestamp = match[2];

        if (match.length === 4 /* && !match[3].length */ ) {

          match[3] = "?";

          obj["m"] = match.join("/");

        } else if (match.length === 6 /* && !match[4].length && !match[5].length */ ) {

          // Sv

          if (!(_data || {}).hasOwnProperty(timestamp)) {

            // regenerate session data

            /*try { // NB. is better if errors in onSave break the app/bot*/

            raw = _onSave();

            /*} catch (e) {

              cb["log"](e.message);

              raw = "";

            }*/

            if (debug) {

              cb["log"](JSON["stringify"](raw));

            }

            _data = {};

            _data[timestamp] = exports["btoa"](exports["unescape"](exports["encodeURIComponent"](raw))); // handle utf8 strings. see http://forums.enyojs.com/discussion/comment/9099/#Comment_9099

            if (!raw) {

              cb["log"]("onSave returned no data.");

            }

          }

          if (_data.hasOwnProperty(timestamp)) { // sanity check

            start = parseInt(match[3], 10);

            chunk = _data[timestamp].slice(start, start + chunk_size);

            match[4] = chunk;

            match[5] = chunk.length;

            obj["m"] = match.join("/");

          }

        } else if (match.length === 7 /* && match[5].length && parseInt(match[5], 10) === match[4].length && !match[6].length */ ) {

          // Ld

          if (debug) {

            cb["log"](parseInt(match[5], 10) === match[4].length);

          }

          if (match[3] === "0") {

            // reset session data

            _data = {};

            _data[timestamp] = "";

          }

          if (_data.hasOwnProperty(timestamp)) { // sanity check

            match[3] = _data[timestamp].length;

            match[6] = match[4].length;

            obj["m"] = match.join("/");

            if (match[4]) {

              _data[timestamp] += match[4];

            } else {

              raw = exports["decodeURIComponent"](exports["escape"](exports["atob"](_data[timestamp]))); // handle utf8 strings. see http://forums.enyojs.com/discussion/comment/9099/#Comment_9099

              if (debug) {

                cb["log"](raw);

              }

              /*try { // NB. is better if errors in onRestore break the app/bot*/

              _onRestore(raw);

              /*} catch(e) {

                cb["log"](e.message)

              }*/

              cb["chatNotice"]("Previously Saved Data Restored.", cb["room_slug"]);

            }

          } // else, if we don't alter obj.m, bookmarklet will detect restore fail

        }

        if (debug) {

          cb["log"](obj["user"] + ": " + obj["m"]);

        }

      }

      obj["X-Spam"] = true;

    } else if (reTag.test(obj["m"])) { // any detected CBS pings are marked as spam

      obj["X-Spam"] = true;

    }

    return obj;

  }

  cb["log"](sSig);

  cb["onMessage"] = function (handler) {

    if (typeof handler !== "function") {

      throw new TypeError(handler + " is not a function");

    }

    _cb_onMessage(function (obj) {

      return handler(message(obj));

    });

    return handler; // allow chaining

  };

  cb["onRestore"] = function (handler) {

    if (typeof handler !== "function") {

      throw new TypeError(handler + " is not a function");

    }

    _onRestore = handler;

    return handler; // allow chaining

  };

  cb["onSave"] = function (handler) {

    if (typeof handler !== "function") {

      throw new TypeError(handler + " is not a function");

    }

    _onSave = handler;

    return handler; // allow chaining

  };

  // set a default onMessage handler

  cb["onMessage"](function (obj) {

    return obj;

  });

  // endof CBS module

}(typeof exports === 'undefined' ? this : exports)); // ignore jslint warning. where this js is going, they don't like === undefined

// don't try to jslint beyond this point. here be dragons!

(function (exports, chars) {

  "use strict";

  // startof base64 module

  /**

  * @constructor

  * @param {string} message

  */

  function InvalidCharacterError(message) {

    this["message"] = message;

  }

  InvalidCharacterError.prototype = new Error();

  InvalidCharacterError.prototype["name"] = "InvalidCharacterError";

  // encoder

  // [https://gist.github.com/999166] by [https://github.com/nignag]

  exports["btoa"] || (exports["btoa"] = function (input) {

    var str = String(input);

    for (

      // initialize result and counter

      var block, charCode, idx = 0, map = chars, output = "";

      // if the next str index does not exist:

      // change the mapping table to "="

      // check if d has no fractional digits

      str.charAt(idx | 0) || (map = "=", idx % 1);

      // "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8

      output += map.charAt(63 & block >> 8 - idx % 1 * 8)) {

      charCode = str.charCodeAt(idx += 3 / 4);

      if (charCode > 0xFF) {

        throw new InvalidCharacterError("\"btoa\" failed: The string to be encoded contains characters outside of the Latin1 range.");

      }

      block = block << 8 | charCode;

    }

    return output;

  });

  // decoder

  // [https://gist.github.com/1020396] by [https://github.com/atk]

  exports["atob"] || (exports["atob"] = function (input) {

    var str = String(input).replace(/=+$/, "");

    if (str.length % 4 == 1) {

      throw new InvalidCharacterError("\"atob\" failed: The string to be decoded is not correctly encoded.");

    }

    for (

      // initialize result and counters

      var bc = 0, bs, buffer, idx = 0, output = "";

      // get next character

      buffer = str.charAt(idx++);

      // character found in table? initialize bit storage and add its ascii value;

      ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,

        // and if not first of each 4 characters,

        // convert the first 8 bits to one ascii character

        bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0 // ignore ClosureCompiler warning

    ) {

      // try to find character in table (0-63, not found => -1)

      buffer = chars.indexOf(buffer);

    }

    return output;

  });

  // endof base64 module

}(typeof exports === 'undefined' ? this : exports, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="));

// NB NB NB *** after closure compiling (debug), remember to convert '%12' => '% 12' or '%0xc' *** ALSO *** re-order app modules to put base64 first ***


Closure Compiler Optimization Notes:


  1. Because the CBS bookmarklet and module share a number of variable constants in common, it is easier to optimize the code as a single unit, the two components being separated out, after optimization. However, due to several factors, the resulting optimized code needs some finessing.
  2. Firstly, the bookmarklet's javascript:/*** CB app/bot data Save'n'restore bookmarklet v2 ***/ protocol label and comment header gets removed by optimization, and should be reinstated.
  3. Secondly, if the code is optimized with the debug variable constant set to true, the optimization of % 12 to %12 in the bookmarklet's time-stamp (ts) function needs to be undone, because it would be interpreted as a form-feed character in a bookmarklet, and throw up an "Uncaught SyntaxError: Unexpected token ILLEGAL" error, if left as optimized.
  4. Thirdly, the optimized Base64 encoder/decoder module is better moved from the end, to the head of the CBS module.
  5. Lastly, the CBS module should be topped and tailed with suitable delimiter comments, before being separated from the bookmarklet.
  6. The Closure Compiler warning that the Base64 encoder/decoder module throws up can safely be ignored, as noted in the comments.


JSLint Tool Use Notes:


  1. Because the latest incarnation of the otherwise excellent JSLint JavaScript Code Quality Tool, lacks most of its predecessor's relaxation options, it is only practical to scan this source code with a previous version.
  2. Even so, there will still be several warnings, all of which can be safely ignored, as noted in the comments.
  3. However, the Base64 encoder/decoder module, at the end of the source code, was not designed to pass JSLint analysis, and should not be checked using it.
  4. Also, the bookmarklet's javascript:/*** CB app/bot data Save'n'restore bookmarklet v2 ***/ protocol label and comment header must be either commented out, or removed, before JSLint analysis.

© Copyright Chaturbate 2011- 2024. All Rights Reserved.