// ==UserScript==
// @name 妖火站内信及帖子回复提醒
// @namespace https://www.yaohuo.me/bbs/userinfo.aspx?touserid=20740
// @version 1.0.6
// @description 自动获取新私信和帖子回复,并在页面内通过Toast弹窗提示用户
// @author SiXi
// @match *://www.yaohuo.me/*
// @match *://yaohuo.me/*
// @icon https://yaohuo.me/css/favicon.ico
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 首次运行时会将所有未读消息和帖子都默认为新数据,直接无视即可,刷新页面或等待8秒后即可自动消失
// 配置项
const CHECK_INTERVAL = 15000; // 15秒更新检查一次数据
const MESSAGE_URL = 'https://www.yaohuo.me/bbs/messagelist.aspx';
const POSTS_URL = 'https://www.yaohuo.me/bbs/book_list_search.aspx?type=pub&classid=0&key=XXXXX'; // 将XXXX替换为你的妖火ID(纯数字,不是用户名)
const ENABLE_POSTS_NOTIFICATION = true; // 是否启用帖子回复提醒功能,true开启,false关闭
// 本地存储的键
const MESSAGES_STORAGE_KEY = 'yaohuoMessages';
const POSTS_STORAGE_KEY = 'yaohuoPosts';
// 创建Toast提醒
function showToast(messages, isPost = false) {
// 先移除旧的toast
const oldToasts = document.querySelectorAll('.toast');
oldToasts.forEach(toast => toast.remove());
// 创建新的toast容器
const toastContainer = document.createElement('div');
toastContainer.className = 'toast-container';
document.body.appendChild(toastContainer);
// 按顺序添加每条消息的toast
messages.forEach((msg, index) => {
const toast = document.createElement('div');
toast.className = 'toast';
if (isPost) {
toast.innerHTML = `
帖子<a href="https://www.yaohuo.me${msg.url}" target="_blank">${msg.title}</a>有新回复,<a href="https://www.yaohuo.me${msg.replyUrl}" target="_blank">点击查看回复列表</a>
`;
} else {
toast.innerHTML = `
新私信:${msg.from} <small>${msg.date}</small><br>
内 容:<a href="https://www.yaohuo.me${msg.url}" target="_blank">${msg.text}</a>
`;
}
toastContainer.appendChild(toast);
// 设置toast动画
setTimeout(() => {
toast.classList.add('show');
}, 100);
// 自动关闭Toast
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
toast.remove();
}, 300);
}, 8000); // Toast消息延迟几秒关闭(单位ms)
});
}
// 添加CSS样式
function addToastStyles() {
const style = document.createElement('style');
style.innerHTML = `
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column-reverse;
gap: 10px;
}
.toast {
background-color: #333;
color: white;
padding: 10px 20px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
transform: translateY(10px);
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.toast a {
color: #4CAF50;
text-decoration: underline;
}
`;
document.head.appendChild(style);
}
// 本地存储获取和设置
function saveMessages(messages) {
GM_setValue(MESSAGES_STORAGE_KEY, messages);
}
function getMessages() {
return GM_getValue(MESSAGES_STORAGE_KEY, []);
}
function savePosts(posts) {
GM_setValue(POSTS_STORAGE_KEY, posts);
}
function getPosts() {
return GM_getValue(POSTS_STORAGE_KEY, []);
}
// 获取站内信
function fetchMessages() {
console.log('正在请求站内信...');
GM_xmlhttpRequest({
method: 'GET',
url: MESSAGE_URL,
onload: function(response) {
console.log('站内信请求成功');
const data = response.responseText;
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
const messageElements = doc.querySelectorAll('.listmms.line1, .listmms.line2');
const newMessages = [];
messageElements.forEach(element => {
// 检查是否有新站内信标志
const newIcon = element.querySelector('img[src="/NetImages/new.gif"]');// 为了简化逻辑,是通过获取是否有新消息图片标识来判断
if (newIcon) {
const linkElement = element.querySelector('a');
const fromElement = element.querySelector('span.laizi');
const dateElement = element.textContent.match(/\d{4}\/\d{1,2}\/\d{1,2} \d{1,2}:\d{2}/);
if (linkElement && fromElement && dateElement) {
const url = linkElement.getAttribute('href');
const text = linkElement.textContent.trim();
const from = fromElement.nextSibling.textContent.trim();
const date = dateElement[0];
newMessages.push({ url, text, from, date });
}
}
});
// 获取本地存储键值与新获取到的数据比对(如果未读消息有new.gif但是本地已经保存了这条消息,那就表示仅仅是未读消息,不进行提醒)
const oldMessages = getMessages();
const uniqueNewMessages = newMessages.filter(msg => !oldMessages.some(old => old.url === msg.url));
if (uniqueNewMessages.length > 0) {
console.log('发现新站内信:', uniqueNewMessages);
// 保存新数据
saveMessages([...oldMessages, ...uniqueNewMessages]);
// 单列平铺显示所有新消息的toast
showToast(uniqueNewMessages);
} else {
console.log('没有新站内信');
}
},
onerror: function(err) {
console.error('获取站内信失败:', err);
showToast([{ from: '系统', date: '', url: '', text: '获取站内信失败' }]);
}
});
}
// 获取帖子回复
function fetchPosts() {
if (!ENABLE_POSTS_NOTIFICATION) return;
console.log('正在请求帖子回复...');
GM_xmlhttpRequest({
method: 'GET',
url: POSTS_URL,
onload: function(response) {
console.log('帖子回复请求成功');
const data = response.responseText;
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
const postElements = doc.querySelectorAll('.listdata.line1, .listdata.line2');
const newPosts = [];
postElements.forEach(element => {
const titleElement = element.querySelector('a.topic-link');
const replyElement = element.querySelector('a.topic-link[href*="book_re.aspx"]');
const replyCount = replyElement ? replyElement.textContent.trim() : '0';
if (titleElement && replyElement) {
const title = titleElement.textContent.trim();
const url = titleElement.getAttribute('href');
const replyUrl = replyElement.getAttribute('href');
newPosts.push({ title, url, replyUrl, replyCount });
}
});
// 比较新帖子回复与本地存储的帖子回复(主要是通过提取每个帖子回帖数量判断是否有增加)
const oldPosts = getPosts();
const updatedPosts = newPosts.filter(newPost => {
const oldPost = oldPosts.find(old => old.url === newPost.url);
return !oldPost || oldPost.replyCount !== newPost.replyCount;
});
if (updatedPosts.length > 0) {
console.log('发现新帖子回复:', updatedPosts);
// 保存新数据
savePosts([...oldPosts, ...updatedPosts]);
// 显示所有新回复的toast
showToast(updatedPosts, true);
} else {
console.log('没有新帖子回复');
}
},
onerror: function(err) {
console.error('获取帖子回复失败:', err);
showToast([{ title: '系统', url: '', replyUrl: '', replyCount: '获取帖子回复失败' }], true);
}
});
}
// 设置定时器
setInterval(() => {
fetchMessages();
fetchPosts();
}, CHECK_INTERVAL);
// 首次加载时立即获取一次(每一次进入妖火任意页面时都会默认先请求一次)
addToastStyles();
fetchMessages();
fetchPosts();
})();