您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a tracker for new messages in ur groups with a list for the groups and their newest messages.
// ==UserScript== // @name Group Messages Tracker // @namespace https://gf.qytechs.cn/en/users/1382956-abara-cadabra // @version 1 // @description Adds a tracker for new messages in ur groups with a list for the groups and their newest messages. // @author Captain // @license MIT // @match *://*.roblox.com/* // @grant GM_xmlhttpRequest // @connect groups.roblox.com // @connect thumbnails.roblox.com // @icon https://icons.iconarchive.com/icons/github/octicons/256/accessibility-inset-16-icon.png // ==/UserScript== (function () { "use strict"; // --- edit these group ids --- // add your group ids here, example: // const groupids = [17221868, 35696958]; const groupids = [id1, id2, id3]; // show counts on home header? true/false per group (better to keep this off lol) const showonhome = [false, false]; // --- don't change anything below unless you know what you're doing --- // --- also, keep in mind, this has not been tested for loading over 10 messages per group or having more then 2 groups listed, I'm unaware of the API's full limitations const apiwall = id => `https://groups.roblox.com/v2/groups/${id}/wall/posts?sortOrder=Desc&limit=100`; const apigroups = ids => `https://groups.roblox.com/v1/groups?groupIds=${ids.join(",")}`; const two_days = 2 * 24 * 60 * 60 * 1000; const now = new Date(); const cachekey = "grouppostsseen"; function formatdate(str) { return new Date(str).toLocaleString(); } function getthumbnails(ids, cb) { if (!ids.length) return cb({}); const url = `https://thumbnails.roblox.com/v1/users/avatar-headshot?userIds=${ids.join(",")}&size=48x48&format=png&isCircular=false`; GM_xmlhttpRequest({ method: "GET", url, onload: res => { try { const data = JSON.parse(res.responseText).data; const map = {}; data.forEach(i => (map[i.targetId] = i.imageUrl)); cb(map); } catch { cb({}); } }, onerror: () => cb({}) }); } function getgroupinfo(ids) { return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url: apigroups(ids), onload: res => { try { const data = JSON.parse(res.responseText).data || []; const map = {}; data.forEach(g => { map[g.id] = { name: g.name, thumbnail: g.thumbnail?.url || null }; }); resolve(map); } catch { resolve({}); } }, onerror: () => resolve({}) }); }); } function getposts(id) { return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url: apiwall(id), onload: res => { try { const data = JSON.parse(res.responseText).data || []; resolve({ id, posts: data.filter(p => p.poster && p.poster.user) }); } catch { resolve({ id, posts: [] }); } }, onerror: () => resolve({ id, posts: [] }) }); }); } function handleposts(id, posts) { const seen = JSON.parse(localStorage.getItem(cachekey) || "[]"); const newposts = posts.filter(p => { const age = now - new Date(p.created); const key = `${id}-${p.id}`; return age < two_days && !seen.includes(key); }); const updated = [...new Set([...seen, ...newposts.map(p => `${id}-${p.id}`)])].slice(-500); localStorage.setItem(cachekey, JSON.stringify(updated)); return { id, total: posts.length, newcount: newposts.length, latest: posts.slice(0, 10) }; } function updateheaders(data) { let newtotal = 0; let alltotal = 0; data.forEach((g, i) => { if (showonhome[i]) { newtotal += g.newcount; alltotal += g.total; } }); if (alltotal && /^\/home\/?$/.test(window.location.pathname)) { const homeheader = document.querySelector("h1[style*='height']"); if (homeheader) homeheader.textContent = `home: (${newtotal} new, ${alltotal} total)`; } const robux = document.querySelector("a.robux-menu-btn"); if (!robux) return; const allnew = data.reduce((acc, g) => acc + g.newcount, 0); const allposts = data.reduce((acc, g) => acc + g.total, 0); robux.textContent = `Group Wall: (${allnew} New, ${allposts} total)`; robux.style.cursor = "pointer"; const clone = robux.cloneNode(true); robux.parentNode.replaceChild(clone, robux); clone.addEventListener("click", e => { e.preventDefault(); showpopup(data); }); } async function showpopup(data) { const groupinfo = await getgroupinfo(groupids); let popup = document.getElementById("grouppopup"); if (popup) popup.remove(); popup = document.createElement("div"); popup.id = "grouppopup"; popup.style = ` position: fixed; top: 80px; right: 20px; width: 420px; max-height: 480px; background: #fff; border: 2px solid #ccc; box-shadow: 0 4px 10px rgba(0,0,0,0.2); padding: 12px; z-index: 9999; font-family: Arial,sans-serif; font-size: 14px; color: #111; border-radius: 6px; display: flex; `; popup.innerHTML = ` <div id="tabs" style="width: 60px; border-right: 1px solid #ddd; display: flex; flex-direction: column; align-items: center; gap: 8px; padding-top: 8px;"></div> <div id="content" style="flex-grow:1; padding-left: 12px; overflow-y: auto; max-height: 440px;"> <strong style="font-size:16px; display:block; margin-bottom:8px;">Latest group posts</strong> <div id="posts">loading...</div> </div> `; const closebtn = document.createElement("button"); closebtn.textContent = "×"; closebtn.title = "close"; closebtn.style = ` position: absolute; top: 6px; right: 8px; border: none; background: none; font-size: 20px; cursor: pointer; color: #999; `; closebtn.onmouseenter = () => (closebtn.style.color = "#333"); closebtn.onmouseleave = () => (closebtn.style.color = "#999"); closebtn.onclick = () => popup.remove(); popup.appendChild(closebtn); document.body.appendChild(popup); const alluserids = [...new Set(data.flatMap(g => g.latest.map(p => p.poster.user.userId)))]; getthumbnails(alluserids, thumbs => { const tabs = popup.querySelector("#tabs"); const postsdiv = popup.querySelector("#posts"); function renderposts(group) { if (!group.latest.length) { postsdiv.innerHTML = '<p style="font-style: italic; color: #666;">no recent posts.</p>'; return; } postsdiv.innerHTML = ""; group.latest.forEach(p => { const uid = p.poster.user.userId; const avatar = thumbs[uid] || `https://www.roblox.com/headshot-thumbnail/image?userId=${uid}&width=48&height=48&format=png&isCircular=false`; const name = p.poster.user.displayName; const role = p.poster.role.name; const date = formatdate(p.created); const div = document.createElement("div"); div.style = "margin-bottom:14px; border-bottom:1px solid #eee; padding-bottom:8px; display:flex; align-items:center;"; div.innerHTML = ` <a href="https://www.roblox.com/users/${uid}/profile" target="_blank" style="flex-shrink:0;"> <img src="${avatar}" alt="${name} avatar" style="width:36px; height:36px; border-radius:6px;" /> </a> <div style="margin-left:12px; flex-grow:1;"> <a href="https://www.roblox.com/users/${uid}/profile" target="_blank" style="font-weight:bold; color:#111; text-decoration:none;"> ${name} </a> <p style="margin:4px 0 2px; white-space:pre-wrap; word-break:break-word;">${p.body}</p> <span style="font-size:0.8em; color:#666;">${role} | ${date}</span> </div> `; postsdiv.appendChild(div); }); } data.forEach((g, i) => { const info = groupinfo[g.id] || {}; const name = info.name || `Group ${g.id}`; const icon = info.thumbnail; const tab = document.createElement("div"); tab.title = name; tab.style = ` width: 48px; height: 48px; border-radius: 8px; cursor: pointer; border: 2px solid transparent; box-sizing: border-box; display: flex; align-items: center; justify-content: center; background: #f0f0f0; position: relative; font-weight: bold; font-size: 24px; color: #444; user-select: none; `; const fallback = i === 0 ? name.charAt(0) : name.charAt(0) + (i + 1); if (icon) { const img = document.createElement("img"); img.src = icon; img.alt = name; img.style = "width:32px; height:32px; border-radius:6px;"; img.onerror = () => { img.remove(); tab.textContent = fallback; }; tab.appendChild(img); } else { tab.textContent = fallback; } tab.addEventListener("click", () => { tabs.querySelectorAll("div").forEach(t => (t.style.borderColor = "transparent")); tab.style.borderColor = "#06c"; renderposts(g); }); tabs.appendChild(tab); if (i === 0) { tab.style.borderColor = "#06c"; renderposts(g); } }); }); } Promise.all(groupids.map(getposts)) .then(results => { const data = results.map(r => handleposts(r.id, r.posts)); updateheaders(data); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址