您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds instant public chat boxes to locales, cities, social clubs and the community page
// ==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://gf.qytechs.cn/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' ); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址