// ==UserScript==
// @name Chaturbate - Better Modding
// @namespace http://www.vpycha.com/gmscripts
// @description Provides a better modding interface for the cam girls site of chaturbate.com. This script makes modding faster and easier. Check out the details.
// @include http://chaturbate.com/*
// @include https://chaturbate.com/*
// @include http://*.chaturbate.com/*
// @include https://*.chaturbate.com/*
// @exclude http://serve.ads.chaturbate.com/*
// @exclude https://serve.ads.chaturbate.com/*
// @grant none
// @run-at document-start
// @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @version 2.12
// ==/UserScript==
// Author: Vladimir Pycha vpycha@gmail.com
// Website: vladpride.cz
// Nickname on chaturbate.com: vlad88x (banned permanently), vlad88z
// Nickname on greasyfork.org: vlad88
// First revision created and released on: July 2013
// The home page of this script is: https://greasyfork.org/scripts/3819-chaturbate-better-modding
// There is also a detailed description of this script there.
function doBetterModdingChanges() {
'use strict';
var messagesCountMax = 200;
var maxMessagesCountColor = 'rgb(255, 0, 0)';
var mostRecentMessagesCount = 4;
var cookieExpirePeriod = 1 * 365;
var pixelsToScrollOffChatBottom = 5;
var expandChar = '+';
var collapseChar = '-';
var collapsedMessageHeight = '10px';
var removedMessageBgColor = 'rgb(225, 225, 225)';
var removedTitle = 'removed';
var silencedTitlePrefix = 'silenced by ';
var bannedTitle = 'kicked out';
var bannedOutlineColor = 'rgb(40, 40, 40)';
var bannedOutlineWidth = '2px';
var bannedOutlineStyle = 'solid';
var pendingSilenceOutlineColor = 'rgb(150, 150, 150)';
var pendingSilenceOutlineWidth = '2px';
var pendingSilenceOutlineStyle = 'solid';
var pendingBanOutlineColor = 'rgb(40, 40, 40)';
var pendingBanOutlineWidth = '2px';
var pendingBanOutlineStyle = 'dashed';
console.info("doing changes in chat room page");
var output = document.createElement('div');
output.setAttribute('id', 'ChaturbateBetterModding');
output.setAttribute('style', 'padding: 0px 8px 10px 8px');
output.innerHTML = 'ChaturbateBetterModding user script is active<br /><span title="In simulation mode you can excersize modding without having to be a mod"><label for="ChaturbateBetterModding_SimulationMode">Simulation mode: </label><input id="ChaturbateBetterModding_SimulationMode" type="checkbox" /></span>';
document.body.appendChild(output);
$.error = console.error;
function strStartsWith(str, prefix) {
return str.substring(0, prefix.length) === prefix;
}
function strEndsWith(str, suffix) {
return str.substring(str.length - suffix.length) === suffix;
}
var oldConfirm = window.confirm;
var silenceMsgTemplateLocalized = gettext('Silence %(username)s?');
window.confirm = function(msg) {
var parts = silenceMsgTemplateLocalized.split('%(username)s');
if (strStartsWith(msg, parts[0]) && strEndsWith(msg, parts[1])) {
var userToSilence = msg.substring(parts[0].length, msg.length - parts[1].length);
var usernameValidationRegExp = /^\S+$/;
if (userToSilence.replace(usernameValidationRegExp, '') === '') {
return true;
}
}
return oldConfirm(msg);
}
var theElement = document.body;
var simulationModeCookieName = 'btmd_sim_mode';
var deletedMessagesShouldBeExpandedCookieName = 'btmd_expand';
var simulationMode = $.cookie(simulationModeCookieName) == '1';
var deletedMessagesShouldBeExpanded = $.cookie(deletedMessagesShouldBeExpandedCookieName) == '1';
setSimulationModeCookie();
setDeletedMessagesShouldBeExpandedCookie();
if (simulationMode) {
console.warn("simulation mode is enabled");
}
function setSimulationModeCookie() {
$.cookie(simulationModeCookieName, simulationMode ? '1' : '0', {
expires: cookieExpirePeriod,
path: '/'
});
}
function setDeletedMessagesShouldBeExpandedCookie() {
$.cookie(deletedMessagesShouldBeExpandedCookieName, deletedMessagesShouldBeExpanded ? '1' : '0', {
expires: cookieExpirePeriod,
path: '/'
});
}
var simulationModeCheckbox = document.getElementById('ChaturbateBetterModding_SimulationMode');
simulationModeCheckbox.checked = simulationMode;
simulationModeCheckbox.onclick = function(evt) {
simulationMode = this.checked;
this.blur();
document.body.focus();
setSimulationModeCookie();
if (simulationMode) {
console.warn("enabled simulation mode");
}
else {
console.info("disabled simulation mode");
}
}
var domele = $(theElement.getElementsByClassName('chat-list')[0]);
var chat = domele.get(0);
var messagesCount = 0;
var previousChatOuterWidth = 0;
var broadcaster = window.broadcaster;
var controlsContainer = document.createElement('div');
controlsContainer.innerHTML = '<input type="button" id="ChaturbateBetterModding_ChatLength" value="' + messagesCount + '" title="Click to delete all messages except the\nlast page, or except the last ' + mostRecentMessagesCount + ' messages\nif clicked with Shift key down" /><br />' +
'<input type="button" id="ChaturbateBetterModding_ToggleExpand" value="' + (deletedMessagesShouldBeExpanded ? collapseChar : expandChar) + '" title="Click to toggle the automatic expanding or collapsing of\ndeleted messages and to expand or collapse all of them\n(keyboard shortcut is <), or click it with Shift key down to\ndo it without the toggling (shortcut is >)" />';
controlsContainer.style.textAlign = 'center';
document.body.appendChild(controlsContainer);
var chatLengthButton = document.getElementById('ChaturbateBetterModding_ChatLength');
var toggleExpandButton = document.getElementById('ChaturbateBetterModding_ToggleExpand');
chatLengthButton.style.paddingLeft = '1px';
chatLengthButton.style.paddingRight = '1px';
toggleExpandButton.style.paddingLeft = '1px';
toggleExpandButton.style.paddingRight = '1px';
var origValue = chatLengthButton.value;
chatLengthButton.value = 266;
var chatLengthButtonMinWidth = $(chatLengthButton).outerWidth(); // really outer width is correct here, I do not know why not inner width
chatLengthButton.value = origValue;
chatLengthButton.style.minWidth = chatLengthButtonMinWidth + 'px';
toggleExpandButton.style.minWidth = chatLengthButtonMinWidth + 'px';
setControlsContainerPosition();
function setControlsContainerPosition() {
var chatPosition = domele.offset();
controlsContainer.style.position = 'absolute';
var chatOuterWidth = domele.outerWidth();
if (previousChatOuterWidth && previousChatOuterWidth - chatOuterWidth == 1) {
// this is a work-aroud
chatOuterWidth = previousChatOuterWidth;
}
else {
previousChatOuterWidth = chatOuterWidth;
}
var left = chatPosition.left + chatOuterWidth + 10;
var top = chatPosition.top;
if (left + controlsContainer.clientWidth > document.body.scrollWidth) {
left = chatPosition.left + chatOuterWidth - controlsContainer.clientWidth - 20;
top -= 9;
}
controlsContainer.style.left = left + 'px';
controlsContainer.style.top = top + 'px';
}
var updateChatLength = function() {
chatLengthButton.value = messagesCount.toString();
if (messagesCount >= messagesCountMax) {
chatLengthButton.style.color = maxMessagesCountColor;
}
else {
chatLengthButton.style.color = '';
}
}
chatLengthButton.onclick = function(evt) {
if (document.activeElement == chatLengthButton) {
chatLengthButton.blur();
document.body.focus();
}
var bigDelete = evt.shiftKey;
var recentMessagesCount;
if (bigDelete) {
recentMessagesCount = mostRecentMessagesCount;
}
else {
recentMessagesCount = 0;
var totalHeight = 0;
var element = chat.lastElementChild;
do {
if (!element) {
break;
}
totalHeight += $(element).outerHeight(true);
recentMessagesCount++;
element = element.previousElementSibling;
} while (totalHeight < chat.clientHeight + pixelsToScrollOffChatBottom);
}
while (messagesCount > recentMessagesCount) {
chat.removeChild(chat.firstElementChild);
messagesCount--;
}
chat.scrollTop = chat.scrollHeight;
updateChatLength();
}
var messagesToCollapse = [];
var removedMessagesOf = null;
chat.addEventListener('scroll', onChatScroll, false);
var removedNickAttr = 'data-removed-nick';
// valid values of attribute: remove, silence, ban
var removeTypeAttr = 'data-remove-type';
var removedByAttr = 'data-removed-by';
function is_at_bottom() {
return chat.scrollTop >= chat.scrollHeight - chat.clientHeight;
}
function onChatScroll(evt) {
if (messagesToCollapse.length > 0 && is_at_bottom()) {
for (var i = messagesToCollapse.length - 1; i >= 0; i--) {
var msgDiv = messagesToCollapse[i];
messagesToCollapse[i] = null;
if (!deletedMessagesShouldBeExpanded && msgDiv && msgDiv.parentNode) {
if (!msgDiv.msgCollapsed) {
collapseMessage(msgDiv);
}
}
messagesToCollapse.length = i;
}
}
}
function getSilencedByTitle(silencer_nick) {
return silencedTitlePrefix + silencer_nick;
}
var removeMessage = function(msgDiv) {
msgDiv.style.background = '';
msgDiv.style.backgroundColor = removedMessageBgColor;
messagesToCollapse[messagesToCollapse.length] = msgDiv;
var paragraph = msgDiv.firstElementChild;
while (paragraph) {
if (paragraph.style.backgroundColor) {
paragraph.style.background = '';
}
paragraph = paragraph.nextElementSibling;
}
};
var on_user_silenced = function (silenced_nick, silencer_nick, index) {
if (index === undefined) {
index = 0;
}
$(".chat-list > div.text > p > [data-nick='" + silenced_nick + "']").each(function (index, value) {
var msgDiv = this.parentNode.parentNode;
if (!msgDiv.hasAttribute(removeTypeAttr) || msgDiv.getAttribute(removeTypeAttr) == 'remove') {
msgDiv.setAttribute(removedNickAttr, silenced_nick);
msgDiv.setAttribute(removeTypeAttr, 'silence');
msgDiv.setAttribute(removedByAttr, silencer_nick);
msgDiv.setAttribute('title', getSilencedByTitle(silencer_nick));
removeMessage(msgDiv);
}
onChatScroll(null);
});
var text = 'User ' + silenced_nick + ' was silenced by ' + silencer_nick;
$.add_system_message(text, domele, index);
};
var on_user_banned = function (username) {
$(".chat-list > div.text > p > [data-nick='" + username + "']").each(function (index, value) {
var msgDiv = this.parentNode.parentNode;
if (msgDiv.getAttribute(removeTypeAttr) != 'ban') {
if (msgDiv.hasAttribute(removedByAttr)) {
msgDiv.removeAttribute(removedByAttr);
}
msgDiv.setAttribute(removedNickAttr, username);
msgDiv.setAttribute(removeTypeAttr, 'ban');
msgDiv.setAttribute('title', bannedTitle);
removeMessage(msgDiv);
msgDiv.style.outlineColor = bannedOutlineColor;
msgDiv.style.outlineWidth = bannedOutlineWidth;
msgDiv.style.outlineStyle = bannedOutlineStyle;
}
onChatScroll(null);
});
var text = 'User ' + username + ' was kicked out of the room';
$.add_system_message(text, domele);
};
var remove_messages = function (username) {
removedMessagesOf = username;
$(".chat-list > div.text > p > [data-nick='" + username + "']").each(function (index, value) {
var msgDiv = this.parentNode.parentNode;
if (!msgDiv.hasAttribute(removeTypeAttr)) {
msgDiv.setAttribute(removedNickAttr, username);
msgDiv.setAttribute(removeTypeAttr, 'remove');
msgDiv.setAttribute('title', removedTitle);
removeMessage(msgDiv);
}
onChatScroll(null);
});
};
var old_add_message;
var message_outbound = null;
var handler;
if (window.TSHandler) {
handler = TSHandler;
}
else {
console.info("using ws_handler instead of TSHandler");
handler = ws_handler;
}
var defchat_message_receiver = null;
var old_connect = handler.connect;
handler.connect = function(message_receiver, room, settings, groups_and_privates) {
defchat_message_receiver = message_receiver;
old_connect(message_receiver, room, settings, groups_and_privates);
};
setTimeout(function() {
if (defchat_settings.handler !== handler) {
handler = defchat_settings.handler;
console.error("bad guess of used handler:", handler);
}
message_outbound = handler.message_outbound;
setControlsContainerPosition();
var message_receiver = defchat_message_receiver;
if (message_receiver === null) {
console.error("not having message_receiver");
}
else {
// these methods are no longer called
//message_receiver.on_user_silenced = on_user_silenced;
//message_receiver.on_user_banned = on_user_banned;
// this method is called instead
message_receiver.remove_messages = remove_messages;
old_add_message = message_receiver.add_message;
messagesCount = chat.children.length;
message_receiver.add_message = add_message;
}
});
function getTitle(msgDiv) {
if (!msgDiv.hasAttribute(removeTypeAttr)) {
return '';
}
switch (msgDiv.getAttribute(removeTypeAttr)) {
case 'remove':
return removedTitle;
case 'silence':
return getSilencedByTitle(msgDiv.getAttribute(removedByAttr));
case 'ban':
return bannedTitle;
default:
return '';
}
}
function expandMessage(msgDiv) {
msgDiv.style.height = '';
for (var i = 0; i < msgDiv.children.length; i++) {
var child = msgDiv.children[i];
child.style.display = '';
}
msgDiv.setAttribute('title', getTitle(msgDiv));
msgDiv.msgCollapsed = false;
}
function collapseMessage(msgDiv) {
for (var i = msgDiv.children.length - 1; i >= 0; i--) {
var child = msgDiv.children[i];
child.style.display = 'none';
}
msgDiv.style.height = collapsedMessageHeight;
var username = msgDiv.getAttribute(removedNickAttr);
var title = getTitle(msgDiv);
msgDiv.setAttribute('title', '' + username + ', ' + title);
msgDiv.msgCollapsed = true;
}
function add_message(message, domeleParam, index) {
if (!domeleParam) {
domeleParam = domele;
}
if (domeleParam.get(0) != chat) {
return old_add_message.call(this, message, domeleParam);
}
if (removedMessagesOf !== null) {
var nick = removedMessagesOf;
removedMessagesOf = null;
if (typeof message == "string") {
var silencer = getSilencer(message, nick);
if (silencer) {
on_user_silenced(nick, silencer);
return;
}
if (isBanText(message, nick)) {
on_user_banned(nick);
return;
}
}
}
var originalScrollTop = chat.scrollTop;
var at_bottom = is_at_bottom();
var result;
var oldFind = domeleParam.find;
domeleParam.find = function(selector) {
if (selector == 'div.text') {
return [];
}
return oldFind.call(this, selector);
};
try {
result = old_add_message.call(this, message, domeleParam, index);
}
finally {
domeleParam.find = oldFind;
}
messagesCount++;
var totalHeight = 0;
while (messagesCount > messagesCountMax) {
var element = chat.firstElementChild;
if (!at_bottom) {
var outerHeight = $(element).outerHeight(true);
totalHeight += outerHeight;
}
chat.removeChild(element);
messagesCount--;
}
if (at_bottom) {
chat.scrollTop = chat.scrollHeight;
}
else {
chat.scrollTop = originalScrollTop - totalHeight;
}
updateChatLength();
return result;
}
function getSilencer(message, nick) {
var slug = 'silencer';
var text = interpolate(gettext("User %(username)s was silenced by %(silencer)s and his/her messages have been removed"), {
username: nick,
silencer: slug
}, true);
var parts = text.split(slug);
var part1Index = message.indexOf(parts[0]);
if (part1Index >= 0) {
var part2Index = message.indexOf(parts[1], part1Index + parts[0].length);
if (part2Index >= 0) {
var silencer = message.substring(part1Index + parts[0].length, part2Index);
if (silencer) {
return silencer;
}
}
}
return null;
}
function isBanText(message, nick) {
var text = gettext('User') + ' ' + nick + ' ' +
gettext('was kicked out of the room and his/her messages have been removed');
return message.indexOf(text) >= 0;
}
chat.addEventListener('dblclick', onChatClick, false);
chat.addEventListener('click', onChatClick, false);
function onChatClick(evt) {
if (evt.button != 0 || evt.shiftKey || evt.altKey || evt.metaKey) {
return;
}
var element = evt.target;
while (element == chat || element.parentNode != chat) {
if (element == chat || !element.parentNode) {
return;
}
element = element.parentNode;
}
if (element.nodeName != 'DIV' || !element.classList.contains('text')) {
return;
}
var msgDiv = element;
var nick = null;
if (msgDiv.hasAttribute(removedNickAttr)) {
nick = msgDiv.getAttribute(removedNickAttr);
}
else {
var paragraph = msgDiv.firstElementChild;
if (paragraph && paragraph.nodeName == 'P') {
var usernameElement = paragraph.firstElementChild;
if (usernameElement && usernameElement.hasAttribute('data-nick')) {
nick = usernameElement.getAttribute('data-nick');
}
}
}
if (!nick) {
return;
}
var ban = evt.ctrlKey;
if (evt.type == 'click') {
// toggling expanded/collapsed state
if (msgDiv.hasAttribute(removeTypeAttr) && !ban) {
if (msgDiv.msgCollapsed) {
expandMessage(msgDiv);
}
else {
collapseMessage(msgDiv);
}
}
}
else
if (evt.type == 'dblclick' && evt.detail == 2) {
// modding
if (!msgDiv.hasAttribute(removeTypeAttr) || ban && msgDiv.getAttribute(removeTypeAttr) != 'ban') {
window.getSelection().removeAllRanges();
evt.stopImmediatePropagation();
evt.stopPropagation();
evt.preventDefault();
if (messagesCount >= messagesCountMax && chat.scrollTop <= 0) {
alert('The chat box is scrolled to the top and there is also the maximum mumber of messages in it, which is ' + messagesCountMax + '. First, either scroll it a little bit down, or delete some of the messages by clicking on that floating button with the number of messages.');
return;
}
if (is_at_bottom() && chat.scrollHeight > chat.clientHeight) {
alert('The chat list is scrolled to the bottom. First, scroll it up by pressing SPACE.');
return;
}
if (!simulationMode) {
if (ban) {
message_outbound.send_kickban_user(nick);
}
var url = '/' + (ban ? 'roomban' : 'roomsilence') + '/' + nick + '/' + broadcaster + '/';
$.post(url, {
'foo': 'bar'
}, function(data, textStatus, jqXHR) {
if (data == 'OK' && textStatus == 'success') {
}
else {
if (ban) {
alert('A ban request has failed.');
}
else {
alert('A silence request has failed.');
}
}
}).fail(function() {
if (ban) {
alert('A ban request has failed.');
}
else {
alert('A silence request has failed.');
}
});
}
else {
window.setTimeout(function() {
/*
if (ban) {
on_user_banned(nick);
}
else {
on_user_silenced(nick, 'nobody');
}
*/
remove_messages(nick);
var text;
if (ban) {
text = gettext('User') + ' ' + nick + ' ' +
gettext('was kicked out of the room and his/her messages have been removed');
}
else {
text = interpolate(gettext("User %(username)s was silenced by %(silencer)s and his/her messages have been removed"), {
username: nick,
silencer: 'nobody'
}, true);
}
$.add_system_message(text, domele);
}, 700);
}
msgDiv.style.outlineColor = ban ? pendingBanOutlineColor : pendingSilenceOutlineColor;
msgDiv.style.outlineWidth = ban ? pendingBanOutlineWidth : pendingSilenceOutlineWidth;
msgDiv.style.outlineStyle = ban ? pendingBanOutlineStyle : pendingSilenceOutlineStyle;
}
}
}
function toggleExpand(noStateChange) {
var expand = !deletedMessagesShouldBeExpanded;
if (!noStateChange) {
toggleExpandButton.value = expand ? collapseChar : expandChar;
deletedMessagesShouldBeExpanded = expand;
setDeletedMessagesShouldBeExpandedCookie();
}
var at_bottom = is_at_bottom();
var msgDiv = chat.firstElementChild;
while (msgDiv) {
if (msgDiv.hasAttribute(removeTypeAttr)) {
if (expand) {
if (msgDiv.msgCollapsed) {
expandMessage(msgDiv);
}
}
else {
if (!msgDiv.msgCollapsed) {
collapseMessage(msgDiv);
}
}
}
msgDiv = msgDiv.nextElementSibling;
};
if (at_bottom) {
chat.scrollTop = chat.scrollHeight;
}
}
theElement.addEventListener('keypress', function(evt) {
var nodeName = evt.target.nodeName;
if (nodeName != 'INPUT' && nodeName != 'TEXTAREA' && nodeName != 'BUTTON') {
var character = String.fromCharCode(evt.charCode);
if (character == ' ') {
if (is_at_bottom()) {
chat.scrollTop = chat.scrollHeight -
chat.clientHeight - pixelsToScrollOffChatBottom;
}
else {
chat.scrollTop = chat.scrollHeight;
}
evt.stopImmediatePropagation();
evt.stopPropagation();
evt.preventDefault();
}
else
if (character == '<' || character == '>') {
var noStateChange = character == '>';
toggleExpand(noStateChange);
}
}
}, false);
toggleExpandButton.onclick = function(evt) {
var noStateChange = evt.shiftKey;
toggleExpand(noStateChange);
if (document.activeElement == toggleExpandButton) {
toggleExpandButton.blur();
document.body.focus();
}
}
}
console.info("ChaturbateBetterModding user script is running");
function contentEval(source) {
'use strict';
// Check for function input.
if (typeof source == 'function') {
// Execute this function with no arguments, by adding parentheses.
// One set around the function, required for valid syntax, and a
// second empty set calls the surrounded function.
source = '(' + source + ')();'
}
// Create a script node holding this source code.
var script = document.createElement('script');
script.setAttribute('type', 'application/javascript');
script.textContent = source;
// Insert the script node into the page, so it will run, and immediately
// remove it to clean up.
document.body.appendChild(script);
document.body.removeChild(script);
}
function getBroadcaster() {
'use strict';
var pathArray = window.location.pathname.split('/');
if (pathArray.length == 3 && pathArray[0] == '' && pathArray[1] != '' && pathArray[2] == '') {
return pathArray[1];
}
else
if (pathArray.length == 4 && pathArray[0] == '' && pathArray[1] == 'b' && pathArray[2] != '' && pathArray[3] == '') {
return pathArray[2];
}
else {
return;
}
}
if (getBroadcaster()) {
var func = function(event) {
if (document.forms.chat_form) {
if (window.$) {
doBetterModdingChanges();
}
else {
// we are running in sandbox
console.info("injecting content script");
contentEval(doBetterModdingChanges);
}
}
};
if (window.defchat_settings) {
// This script was injected way too late.
// Will not be able to get message_receiver.
console.error("defchat already initialized");
func();
}
else
if (window.$) {
// jQuery was loaded before this script. It sometimes happens in Chromium.
// Need to use jQuery's handler of DOMContentLoaded event.
console.warn("using jQuery's handler of DOMContentLoaded event");
$(func);
}
else {
// the normal case
document.addEventListener('DOMContentLoaded', func);
}
}
// vim: tabstop=2:shiftwidth=2:expandtab