document.write(`
`);
/* ===== ES6 Shoutbox Client ===== */
class Chat {
constructor(room, username = '', avatar) {
this.server = 'https://www.shoutbox.com';
this.AJAX = `${this.server}/chat/ajax.php`;
this.myUser = {
username,
room,
avatar: avatar || `${this.server}/avatars/${Math.ceil(Math.random() * 29)}.svg`,
password: '',
id: Date.now(),
isAdmin: false
};
this.users = {};
window.shoutbox = this;
this.traductions = {
welcome: "Welcome %s. ",
userOnline: "%s user online",
usersOnline: "%s users online",
enterYourTextHere: "Enter your text here",
serverMessage: "%s
",
enterAdminPassword: "Enter admin password",
imageAvatar: "",
youAreAdminNow: "You are admin now.",
mp3: "https://www.shoutbox.com/chat/mp3/dink.mp3",
addUser: "",
banText: "",
receivedText: "%s%s %s: %s
",
youBannedUser: "You banned %s"
};
this.smileys = {
':)': '🙂', ';)': '🙂', ':D': '😃', 'xD': '😆', ':(': '😟', ":'(": '😢',
'>:(': '😠', ':O': '😮', ':$': '😳', ':|': '😐', '<3': '❤'
};
// quick sprintf
this.sprintf = (str, ...argv) => !argv.length ? str : this.sprintf(str.replace(/%s/, argv.shift()), ...argv);
// polyfill replaceAll if needed
if (!String.prototype.replaceAll) {
// eslint-disable-next-line no-extend-native
String.prototype.replaceAll = function (target, replacement) { return this.split(target).join(replacement); };
}
// Prefill admin fields
try {
const email = localStorage.getItem('email');
const pwd = localStorage.getItem('password');
if (email) jQuery('#shoutboxEmailAdmin').val(email);
if (pwd) jQuery('#shoutboxPasswordAdmin').val(pwd);
} catch (_) {}
this.bindUI();
this.initSocket();
this.bootstrap();
}
/* ---------- UI helpers ---------- */
parseSmileys = (text) => {
Object.keys(this.smileys).forEach(s => { text = text.replaceAll(s, this.smileys[s]); });
return text;
};
stripHTML = (html) => {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
clearChat = () => { const el = document.querySelector('#shoutChat'); if (el) el.innerHTML = ''; };
serverMessage = (text) => {
const $shoutChat = jQuery('#shoutChat');
$shoutChat.append(this.sprintf(this.traductions.serverMessage, text));
$shoutChat.animate({ scrollTop: $shoutChat[0].scrollHeight }, 300);
};
showAd = () => {
const html = `Get your free shoutbox with no ads for 9.90€/year`;
this.serverMessage(html);
};
getColor = (username) => {
const colors = ['#FFB900','#D83B01','#B50E0E','#E81123','#B4009E','#5C2D91','#0078D7','#00B4FF','#008272','#107C10'];
let sum = 0;
for (let i = 0; i < username.length; i++) sum += username.charCodeAt(i);
return colors[sum % colors.length];
};
getAvatar = (image, username = '') => {
const color = this.getColor(username);
if (image.indexOf(this.server) === 0) {
const firstLetter = (username.charAt(0) || '').toUpperCase();
const secondLetter = (username.charAt(1) || '').toUpperCase();
return ``;
}
return this.sprintf(this.traductions.imageAvatar, image);
};
receiveText = (username, message, date, scrollTimer, avatar, ip, id) => {
username = this.stripHTML(username || '');
message = this.parseSmileys(message || '');
if (avatar) avatar = this.getAvatar(avatar, username);
if (date) date = this.sprintf('(%s)', date);
const html = this.sprintf(this.traductions.receivedText, id, ip, avatar || '', date || '', username, message);
const $shoutChat = jQuery('#shoutChat');
$shoutChat.animate({ scrollTop: $shoutChat[0].scrollHeight }, scrollTimer || 0);
jQuery(html).hide().appendTo('#shoutChat').fadeIn(200);
if (this.myUser.isAdmin) jQuery(`div[data-id="${id}"]`).addClass('shoutboxAdmin');
};
addUser = (user) => {
if (!user.username) user.username = this.getRandomUsername();
this.updateNumberUsersDisplay();
let avatar = user.avatar;
if (avatar) avatar = this.getAvatar(avatar, user.username);
const txt = this.sprintf(this.traductions.addUser, user.id, user.id, avatar || '', user.username);
jQuery('#shoutBoxUserList').append(txt);
};
updateNumberUsersDisplay = () => {
const len = Object.keys(this.users || {}).length;
const text = (len > 1)
? this.sprintf(this.traductions.usersOnline, len)
: this.sprintf(this.traductions.userOnline, len);
jQuery('#shoutBoxHeaderText').text(text);
};
getRandomUsername = () => {
const a = ['Small','Blue','Ugly','Big','Red','Yellow','Green','Nice','Cool'];
const b = ['Bear','Dog','Banana','John','Joe','Jack','Chatter','Fish','Bird'];
return `${a[Math.floor(Math.random()*a.length)]}${b[Math.floor(Math.random()*b.length)]}`;
};
welcome = () => {
const $in = jQuery('#shoutBoxInput');
$in.attr('placeholder', this.traductions.enterYourTextHere);
this.serverMessage(this.sprintf(this.traductions.welcome, this.myUser.username));
$in.removeClass('shoutInputRed');
try { localStorage.setItem('username', this.myUser.username); } catch (_) {}
};
/* ---------- Data flows ---------- */
refreshChat = async () => {
this.clearChat();
const fd = new FormData();
fd.append('a', 'getLastMessages');
fd.append('id', this.myUser.room);
const res = await fetch(this.AJAX, { method: 'POST', body: fd });
const messages = await res.json().catch(() => []);
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
this.receiveText(m.username, m.message, m.date, 0, m.avatar, m.ip, m.id);
}
};
getLastMessages = async () => {
const fd = new FormData();
fd.append('a', 'getLastMessages');
fd.append('id', this.myUser.room);
const res = await fetch(this.AJAX, { method: 'POST', body: fd });
const messages = await res.json().catch(() => []);
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
this.receiveText(m.username, m.message, m.date, 0, m.avatar, m.ip, m.id);
}
if (this.myUser.username) this.welcome();
};
/* ---------- Socket.IO ---------- */
initSocket = () => {
this.shoutboxSocket = io.connect(`${this.server}:8443`);
this.shoutboxSocket.on('connect', () => {
const stored = this.stripHTML(localStorage.getItem('username') || '');
if (stored) this.myUser.username = stored;
this.shoutboxSocket.emit('enterRoom', this.myUser);
});
this.shoutboxSocket.on('roomEntered', () => {
const stored = this.stripHTML(localStorage.getItem('username') || '');
if (stored) this.welcome();
});
this.shoutboxSocket.on('del', (id) => jQuery(`[data-id="${id}"]`).remove());
// FIX: correct selector (quote IP attr)
this.shoutboxSocket.on('ban', (ip) => jQuery(`[data-ip="${ip}"]`).remove());
this.shoutboxSocket.on('receiveText', (user, message, ip, id) => {
this.receiveText(user.username, message, '', 200, user.avatar, ip, id);
try { new Audio(this.traductions.mp3).play(); } catch (_) {}
});
this.shoutboxSocket.on('userChanged', (user) => {
let avatar = user.avatar ? this.getAvatar(user.avatar, user.username) : '';
const txt = this.sprintf(this.traductions.addUser, user.id, user.id, avatar, user.username);
jQuery(`#shoutBoxUser${user.id}`).html(txt);
});
this.shoutboxSocket.on('setAdminMode', (password) => {
this.setAdminMode(password);
jQuery('div.shoutText').addClass('shoutboxAdmin');
});
this.shoutboxSocket.on('addUser', (user) => {
this.users[user.id] = user;
this.addUser(user);
});
this.shoutboxSocket.on('removeUser', (user) => {
delete this.users[user.id];
this.updateNumberUsersDisplay();
jQuery(`#shoutBoxUser${user.id}`).remove();
});
this.shoutboxSocket.on('error', (err) => console.log(err));
};
/* ---------- Admin & actions ---------- */
setAdminMode = (password) => {
this.myUser.password = password;
this.myUser.isAdmin = true;
this.serverMessage(this.traductions.youAreAdminNow);
jQuery('#shoutboxAdminLoginBtn').toggle();
};
sendText = () => {
const $in = jQuery('#shoutBoxInput');
let text = ($in.val() || '').trim();
text = this.stripHTML(text);
if (!text) return;
if (!this.myUser.username) {
this.myUser.username = text;
$in.val('');
this.welcome();
this.shoutboxSocket.emit('changeUser', this.myUser);
return;
}
$in.val('');
this.shoutboxSocket.emit('send', this.myUser, text);
$in.prop('disabled', true);
setTimeout(() => { $in.prop('disabled', false); $in.focus(); }, 800);
};
/* ---------- Bind UI ---------- */
bindUI = () => {
const $container = jQuery('.shoutBoxContainer');
const $loginPanel = jQuery('#shoutboxLoginPanel');
const $pwdChange = jQuery('#shoutboxAdminPasswordChangePanel');
const $forgotten = jQuery('#shoutboxForgottenPassword');
jQuery('#shoutBoxInput').on('keypress', (e) => { if ((e.keyCode || e.which) === 13) this.sendText(); });
$container.on('click', '.shoutboxBanBtn', (e) => {
const $el = jQuery(e.currentTarget);
const ip = $el.closest('div').data('ip');
this.shoutboxSocket.emit('ban', ip);
const uname = $el.parent().find('.shoutUserText').text();
this.serverMessage(this.sprintf(this.traductions.youBannedUser, uname));
});
$container.on('click', '.shoutboxDelBtn', (e) => {
const id = jQuery(e.currentTarget).closest('div').data('id');
this.shoutboxSocket.emit('del', id);
});
jQuery(document).on('click', '.shoutboxChangeUsernameBtn', () => {
localStorage.clear();
this.myUser.username = '';
const $in = jQuery('#shoutBoxInput');
$in.addClass('shoutInputRed').val('').attr('placeholder', 'Enter new username').focus();
});
jQuery('#shoutboxForgottenBtn').click(() => $forgotten.slideToggle(200));
jQuery('#shoutboxAdminLoginBtn').click((e) => {
e.stopImmediatePropagation();
if (this.myUser.isAdmin) {
$loginPanel.hide();
$pwdChange.slideToggle();
$forgotten.hide();
} else {
$loginPanel.slideToggle(200);
$pwdChange.hide();
$forgotten.hide();
}
});
jQuery('#shoutBoxHeader').click(() => jQuery('#shoutBoxUserList').toggle('fast'));
jQuery('#shoutboxSaveConfigBtn').click(async (e) => {
const $_err = jQuery(e.currentTarget).closest('.panel, .configPanel').find('.error');
const mustRegister = jQuery('#shoutboxUserMustRegister').is(':checked');
const oldPwd = jQuery('#shoutboxChangeOldPassword').val();
const newPwd = jQuery('#shoutboxChangeNewPassword').val();
if ((oldPwd || '').length < 3 || (newPwd || '').length < 3) {
this.displayError($_err, 'Invalid Password'); return;
}
jQuery('.error').empty();
const fd = new FormData();
fd.append('a', 'updateAdmin');
fd.append('shoutboxUserMustRegister', mustRegister);
fd.append('oldPassword', oldPwd);
fd.append('newPassword', newPwd);
const res = await fetch(this.AJAX, { method: 'POST', body: fd });
let txt = await res.text();
if (txt === 'ko') { this.displayError($_err, 'Invalid email/password'); return; }
let user;
try { user = JSON.parse(txt); } catch { user = null; }
if (!user) { this.displayError($_err, 'Server error'); return; }
this.myUser.password = user.password;
this.myUser.shoutboxUserMustRegister = user.shoutboxUserMustRegister;
$pwdChange.slideToggle(200);
$loginPanel.hide();
});
jQuery('#shoutboxLoginAdminBtn').click(async (e) => {
const $_err = jQuery(e.currentTarget).closest('.panel, .loginPanel').find('.error');
const email = jQuery('#shoutboxEmailAdmin').val();
const password = jQuery('#shoutboxPasswordAdmin').val();
if ((password || '').length < 3) { this.displayError($_err, 'Invalid Password'); return; }
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!re.test(email || '')) { this.displayError($_err, 'Invalid email'); return; }
jQuery('.error').empty();
const fd = new FormData();
fd.append('a', 'loginAdmin'); fd.append('email', email); fd.append('password', password);
const res = await fetch(this.AJAX, { method: 'POST', body: fd });
let txt = await res.text();
if (txt === 'ko') { this.displayError($_err, 'Invalid email/password'); return; }
let user;
try { user = JSON.parse(txt); } catch { user = null; }
if (!user) { this.displayError($_err, 'Server error'); return; }
this.myUser.username = 'admin';
this.myUser.password = user.password;
this.myUser.shoutboxUserMustRegister = user.shoutboxUserMustRegister;
this.myUser.isAdmin = true;
this.myUser.avatar = `${this.server}/avatars/admin.svg`;
$loginPanel.hide();
this.serverMessage(this.traductions.youAreAdminNow);
try { localStorage.setItem('email', email); localStorage.setItem('password', password); } catch {}
this.shoutboxSocket.emit('checkPassword', password);
});
jQuery('#sendMyPasswordBtn').click(async () => {
const $_err = jQuery('#shoutboxForgottenPassword').find('.error');
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const email = jQuery('#shoutboxForgottenEmail').val();
if (!re.test(email || '')) { this.displayError($_err, 'Invalid email'); return; }
const fd = new FormData();
fd.append('a', 'forgottenshoutboxPasswordAdmin');
fd.append('email', email);
const res = await fetch(this.AJAX, { method: 'POST', body: fd });
const txt = await res.text();
if (txt === 'ko') { this.displayError($_err, 'No such email !'); return; }
jQuery('#shoutboxForgottenPassword').hide(200);
});
jQuery('.shoutBoxContainer').on('click', '.shoutBoxUserItem', (e) => {
e.stopImmediatePropagation();
const userId = jQuery(e.currentTarget).data('id');
this.openPrivateChat(userId);
});
};
displayError = ($el, message) => { $el.html(message); setTimeout(() => $el.empty(), 3000); };
openPrivateChat = (userid) => { /* TODO: DM */ };
/* ---------- First data bootstrap ---------- */
bootstrap = async () => {
await this.getLastMessages();
// Webmaster check
const fd = new FormData();
fd.append('a', 'getWebmaster');
fd.append('id', this.myUser.room);
const res = await fetch(this.AJAX, { method: 'POST', body: fd });
const data = await res.json().catch(() => ({}));
if (data['squat'] === 'squat') { window.location = this.server; return; }
if (!data['paid']) {
if (parseInt(data.entries || '0', 10) > 50) this.showAd();
setInterval(() => this.showAd(), 30000);
}
};
}
/* ---- Auto-start if script filename ends with numeric webmaster id ---- */
(() => {
const src = document.currentScript && document.currentScript.src;
let webmasterid = 0;
if (src) {
const tail = src.split('/').pop();
webmasterid = Number(tail);
}
if (Number.isInteger ? Number.isInteger(webmasterid) : (typeof webmasterid === 'number' && isFinite(webmasterid) && Math.floor(webmasterid) === webmasterid)) {
if (webmasterid > 0) setTimeout(() => { new Chat(webmasterid); }, 1000);
}
})();