document.write(`
Forgotten password ?
Enter email
Register for new account
Forgotten password ?
Enter email
`); /* ===== 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: "
%s %s
", 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 `
${firstLetter}${secondLetter}
`; } 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); } })();