Popmundo chat

Adds instant public chat boxes to locales, cities, social clubs and the community page

Instalar este script¿?
Script recomendado por el autor

Puede que también te guste Popmundo Dark Theme.

Instalar este script
// ==UserScript==
// @name         Popmundo chat
// @name:tr      Popmundo Sohbet
// @namespace    bheuv.dev
// @version      1.4
// @description  Adds instant public chat boxes to locales, cities, social clubs and the community page
// @description:tr Mekanlara, şehirlere, sosyal kulüplere ve hoş geldiniz sayfasına anlık sohbet edebileceğiniz odalar ekler.
// @author       Ian Parsons (105997)
// @match        https://*.popmundo.com/*
// @icon         https://www.google.com/s2/favicons?domain=popmundo.com
// @require      https://unpkg.com/[email protected]/client-dist/socket.io.min.js
// @require      https://unpkg.com/[email protected]/dist/vue.global.prod.js
// @require      https://unpkg.com/[email protected]/dayjs.min.js
// @require      https://unpkg.com/[email protected]/dist/umd/uuidv4.min.js
// @run-at       document-end
// @noframes
// ==/UserScript==
const hostOrigin = 'https://botmundo.bheuv.dev:3005';
const chatImagePath = 'https://botmundo.bheuv.dev/pm-chat'; // Hosting this seperately because Popmundo does not links that specify a port
const scriptLink = 'https://greasyfork.org/en/scripts/431380-popmundo-chat';

(function() {
    'use strict';

    let parseCharacterData = function() {
        // This must be done to make the authenticated character known to the script
        try {
            if (window.location.href.indexOf('/ChooseCharacter') !== -1) {
                // The select character page discloses the character ids
                let content = document.getElementById('ppm-content');
                let buttons = content.querySelectorAll('input[type="submit"]');
                let characterMap = [];

                for (let i = 0, len = buttons.length; i < len; i++) {
                    let id = buttons[i].parentNode.parentNode.querySelector("div.idHolder").innerText;
                    let name = buttons[i].parentNode.parentNode.querySelector("h2 a").innerText;
                    characterMap[i] = id + ':' + name;
                }

                // Store the data for use on other pages
                window.localStorage.setItem('characterMap', characterMap.join(','));
            } else {
                let characterMap = window.localStorage.getItem('characterMap');

                if (! characterMap) {
                    // Character map is not available; cannot run the script
                    throw new Error("Character map is unavailable!");
                }

                // Charactermap will now be an array of characters with format [id:name]
                characterMap = characterMap.split(',');

                const dropdown = document.querySelector('#character-tools-character select');
                const options = dropdown.querySelectorAll('option');

                const names = [];
                const ids = [];

                // Split character map into a list of names and ids to make searching in the next step easier
                characterMap.forEach((character) => {
                    const [id, name] = character.split(':');
                    names.push(name);
                    ids.push(id);
                });

                // Attempt to match each option in the dropdown menu to a name from the list of names and set the ID that matches that name's index
                for (let i = 0, len = options.length; i < len; i++) {
                    const option = options[i];
                    const index = names.indexOf(option.innerText);

                    if (index !== -1) {
                        option.dataset.id = ids[index];
                    }
                }

                // Now find the authenticated character's id and attach it as data to the document body
                let selectedOption = dropdown.querySelector('option[selected]');
                let selectedValue = selectedOption.dataset.id;

                if (selectedValue) {
                    document.body.dataset.character_name = selectedOption.innerText;
                    document.body.dataset.character_id = selectedValue;
                } else {
                    throw "Failed to parse character data from character select box!";
                }
            }
        } catch (e) {
            console.log("CharacterMap could not be found/built: " + e);
            console.log(e);
        }
    }

    if (! document.querySelector('body').dataset.character_id) {
        parseCharacterData();
    }

    if (! document.querySelector('body').dataset.character_id) {
        // Fallback method
        if (document.location.href.endsWith('.popmundo.com/World/Popmundo.aspx/Character')) {
            const characterName = document.querySelector('div.charPresBox h2').innerText;
            const characterIdentifier = document.querySelector('.idHolder').innerText;

            window.localStorage.setItem('character_name', characterName);
            window.localStorage.setItem('character_id', characterIdentifier);
        }

        if (window.localStorage.getItem('character_id')) {
            document.body.dataset.character_id = window.localStorage.getItem('character_id');
            document.body.dataset.character_name = window.localStorage.getItem('character_name');
        }
    }

    const characterName = document.body.dataset.character_name;
    const characterIdentifier = document.body.dataset.character_id;

    const addStyle = function(style) {
        const styleEl = document.createElement('style');
        styleEl.textContent = style;
        document.head.append(styleEl);
    }

    // Resize property doesn't work on iframes in firefox - this is a workaround
    addStyle(`
    #chat {
        display: grid;
        grid-template-areas:
            "history"
            "controls";
        grid-template-rows: 1fr auto;
        height: 400px;
        min-height: 400px;
        resize: vertical;
        overflow: hidden;
        font-size: 12px;
        font-weight: 400;
        line-height: 1.5;
        font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
    }
    #chat, #chat *, #chat *::before, #chat *::after {
        box-sizing: border-box;
    }
    .message {
        padding: 0.5em;
        background-color: #40444b;
        color: #fff;
        border-radius: 5px;
        margin: 0.25em 0.5em;
        opacity: 1;
        animation: fade 0.15s linear;
    }
    .history {
        grid-area: history;
        max-height: 100%;
        height: 100%;
        margin-top: auto;
        width: 100%;
        overflow-x: hidden;
        overflow-y: scroll;
    }
    .controls {
        grid-area: controls;
        width: 100%;
    }
    #chat.hidden {
        display: none !important;
    }
    .message-author {
        font-weight: bold;
        text-decoration: none;
    }
    .client {
        margin: 0.125em;
        opacity: 1;
        animation: fade 0.15s linear;
    }
    .timestamp {
        float: right;
        user-select:none;
    }
    .clients {
        max-height: 4.5em;
        overflow-y: auto;
        overflow-x: hidden;
    }
    @keyframes fade {
        0% { opacity: 0 }
        100% { opacity: 1 }
    }

    .p-2 {
        padding: 0.5em !important;
    }
    .px-2 {
        padding-right: 0.5em !important;
        padding-left: 0.5em !important;
    }
    .text-center {
        text-align: center !important;
      }

@-webkit-keyframes spinner-border {
    to {
      transform: rotate(360deg) /* rtl:ignore */;
    }
  }

  @keyframes spinner-border {
    to {
      transform: rotate(360deg) /* rtl:ignore */;
    }
  }
  .spinner-border {
    display: inline-block;
    width: 2em;
    height: 2em;
    vertical-align: -0.125em;
    border: 0.25em solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    -webkit-animation: 0.75s linear infinite spinner-border;
    animation: 0.75s linear infinite spinner-border;
  }
  .text-primary {
    color: rgba(13, 110, 253, 1) !important;
  }
.text-info {
    color: rgba(13, 202, 240, 1) !important;
  }
  .text-muted {
    color: #6c757d !important;
  }
  .visually-hidden {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
  }
  .badge {
    display: inline-block;
    padding: 0.35em 0.65em;
    font-size: 0.75em;
    font-weight: 700;
    line-height: 1;
    color: #fff;
    text-align: center;
    white-space: nowrap;
    vertical-align: baseline;
    border-radius: 0.25em;
  }
  .badge:empty {
    display: none;
  }
  .alert {
    position: relative;
    padding: 1em 1em;
    margin-bottom: 1em;
    border: 1px solid transparent;
    border-radius: 0.25em;
  }
  .alert-danger {
    color: #842029;
    background-color: #f8d7da;
    border-color: #f5c2c7;
  }
  .bg-primary {
    background-color: rgba(13, 110, 253, 1) !important;
  }
  .bg-success {
    background-color: rgba(25, 135, 84) !important;
  }
  .form-control {
    display: block;
    width: 100%;
    padding: 0.375em 0.75em;
    font-size: 1em;
    font-weight: 400;
    line-height: 1.5;
    color: #212529;
    background-color: #fff;
    background-clip: padding-box;
    border: 1px solid #ced4da;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    border-radius: 0.25em;
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
  }
  .form-control:focus {
    color: #212529;
    background-color: #fff;
    border-color: #86b7fe;
    outline: 0;
    box-shadow: 0 0 0 0.25em rgba(13, 110, 253, 0.25);
  }
  .form-control::placeholder {
    color: #6c757d;
    opacity: 1;
  }

    `);

    const createChatBox = function(resourceType, resourceIdentifier, characterIdentifier, characterName, mountCallback, chatTitle)
    {
        if (!chatTitle) {
            chatTitle = 'Botmundo Chat';
        }

        const wrapper = document.createElement('div');
        wrapper.classList.add('box');
        wrapper.innerHTML = `
            <h2>${chatTitle}</h2>
            <div id="loader" class="text-center p-5">
                <div class="spinner-border text-primary" role="status">
                    <span class="visually-hidden">Loading...</span>
                </div>
            </div>
            <div id="chat" class="hidden">
                <div class="history" ref="history">
                    <div class="message" v-for="message in messages" data-uud="message.uuid">
                        <a class="message-author text-info" :href="characterURL(message.character.identifier)">{{ message.character.name }}</a>
                        <span class="timestamp text-muted">{{ renderTimestamp(message.timestamp) }}</span>
                        <div class="message-content">{{ message.content }}</div>
                    </div>
                </div>
                <div class="controls">
                    <div class="px-2 clients">
                        People here: <a class="client" :href="characterURL(client.identifier)" v-for="client in clients">
                            <span :class="{'badge': true, 'bg-primary': client.identifier !== user.identifier, 'bg-success': client.identifier === user.identifier}">{{ client.name }}</span>
                        </a>
                    </div>
                    <form @submit.prevent="sendMessage">
                        <div class="p-2">
                            <input class="form-control" type="text" v-model="message" placeholder="Say something..." maxlegth="1024" />
                        </div>
                    </form>
                </div>
            </div>
        `;

        mountCallback(wrapper);
        connect(wrapper, {
            character: {
                identifier: characterIdentifier,
                name: characterName
            },
            resource: {
                identifier: resourceIdentifier,
                type: resourceType
            }
        });
    }

    const connect = (element, data) => {
        const baseUrl = document.location.origin;
        const chatWindow = element.querySelector('#chat');
        const loader = element.querySelector('#loader');

        const socket = io(hostOrigin, {
            auth: {
                user_identifier: data.character.identifier,
                user_name: data.character.name,
                user_token: null,
                resource_type: data.resource.type,
                resource_identifier: data.resource.identifier
            }
        });

        socket.on('connect_error', (data) => {
            loader.innerHTML = `<div class="alert alert-danger" role="alert"><strong>Cannot connect:</strong><br /><p>${data}</p></div>`;
        });

        socket.on('connect', () => {
            // Initialize UI
            unsafeWindow.Vue = Vue;
            const app = Vue.createApp({
                props: ['user'],
                data() {
                    return {
                        message: '',
                        messages: [],
                        clients: [],
                        autoScroll: true
                    }
                },
                methods: {
                    sendMessage() {
                        if (this.message.length < 1) {
                            return;
                        }

                        socket.emit('message', {
                            content: this.message
                        });

                        this.message = '';
                    },

                    characterURL(id) {
                        return `${baseUrl}/World/Popmundo.aspx/Character/${id}`;
                    },
                    renderTimestamp(timestamp) {
                        return dayjs(timestamp).format('MMM DD @ HH:mm');
                    }
                },
                mounted() {
                    socket.on('message', (message) => {
                        this.messages.push(message);

                        const element = this.$refs.history;
                        if (element.scrollTop === (element.scrollHeight - element.offsetHeight)) {
                            // User was scrolled all the way down - ensure it stays that way
                            this.$nextTick(() => {
                                this.$refs.history.scrollTop = this.$refs.history.scrollHeight;
                            });
                        }
                    });

                    socket.on('history', (message) => {
                        this.messages = message.messages;
                        this.$nextTick(() => {
                            this.$refs.history.scrollTop = this.$refs.history.scrollHeight;
                        });
                    });

                    socket.on('clients', (message) => {
                        // Remove duplicate names
                        const set = new Set();
                        this.clients = message.clients.filter(c => {
                            if (set.has(c.identifier)) return false;
                            set.add(c.identifier);
                            return true;
                        });
                    });
                }
            }, {user: data.character});

            chatWindow.classList.remove('hidden');
            loader.classList.add('hidden');
            app.mount(chatWindow);
        });
    }

    (function() {
        // Add chat button on editor
        const editors = document.querySelectorAll('.editor .buttons:nth-of-type(1)');

        const generateChatCode = function(event) {
            event.preventDefault();
            const chatCode = `[image=${chatImagePath}/${uuidv4()}.png link=${scriptLink}]`;
            const editor = event.target.parentElement.parentElement.querySelector('textarea');
            editor.value = editor.value.substring(0, editor.selectionStart) + chatCode + editor.value.substring(editor.selectionEnd, editor.value.length);
        };

        for (let editor of editors) {
            const element = document.createElement('a');
            element.innerText = 'Chat';
            element.classList.add('button');
            element.href = '#';
            element.addEventListener('click', generateChatCode);
            editor.append(element);
        }

        const customCandidates = document.querySelectorAll('div.tbc a img.userimage');
        const customRegex = /([a-zA-Z0-9\-]+).png/i;

        for (let candidate of customCandidates) {
            if (! candidate.src.startsWith(chatImagePath)) continue;
            const match = customRegex.exec(candidate.src);
            if (!match) continue;

            const identifier = match[1];
            const linkElement = candidate.parentElement;

            createChatBox(
                'custom',
                identifier,
                characterIdentifier,
                characterName,
                (el) => {
                    linkElement.parentNode.replaceChild(el, linkElement);
                },
                `Custom Chat - ${identifier}`
            );
        }
    })();

    if (/Locale(\/[0-9]+$|$)/.test(document.location.href)) {
        // Locale page
        const localeIdentifier = parseInt(document.querySelector('.idHolder').innerText);
        if (localeIdentifier) {
            createChatBox(
                'locale',
                localeIdentifier,
                characterIdentifier,
                characterName,
                (el) => {
                    document.querySelector("#ppm-content h1").insertAdjacentElement('afterend', el);
                },
                document.title.split(' - ')[1] + ' Chat'
            );
        }
    }
    else if (document.location.href.endsWith('/World/Popmundo.aspx')) {
        // Community news / Welcome page
        createChatBox(
            'community',
            0,
            characterIdentifier,
            characterName,
            (el) => {
                document.querySelector("div.newsList").insertAdjacentElement('beforebegin', el);
            },
            'Global Chat'
        );
    } else if (document.querySelector("#ctl00_cphLeftColumn_ctl00_pnlCalendar")) {
        // City page
        const cityIdentifier = document.querySelector("#ctl00_cphRightColumn_ctl01_ddlCities").value;
        createChatBox(
            'city',
            cityIdentifier,
            characterIdentifier,
            characterName,
            (el) => {
                document.querySelector("#ppm-content h1").insertAdjacentElement('afterend', el)
            },
            document.title.split(' - ')[1] + ' Chat'
        );
    } else if (/SocialClub\/[0-9]+$/.test(document.location.href)) {
        // Social club
        const clubIdentifier = document.querySelector(".idHolder").innerText;

        createChatBox(
            'club',
            clubIdentifier,
            characterIdentifier,
            characterName,
            (el) => {
                document.querySelector("#ppm-content h1").insertAdjacentElement('afterend', el)
            },
            document.querySelector("#ppm-content h1").innerText + ' Chat'
        );
    }
})();