您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Get notified when specific Reddit activities occur
// ==UserScript== // @name Reddit Activity Monitor // @namespace https://justanotherenemy.com // @description Get notified when specific Reddit activities occur // @match https://*.reddit.com/* // @grant GM_notification // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @version 1.0 // ==/UserScript== // Initial configuration let CONFIG = GM_getValue('redditMonitorConfig', { monitorSubreddits: ['insertsubredditname'], monitorUsers: ['INSERT-USER-NAME'], monitorThreads: [], refreshInterval: 60000, // 1 minute in ms maxNotifications: 5, enabled: true }); // Store for previously seen content let seenContent = GM_getValue('seenContent', { posts: {}, comments: {} }); // Add styles for the UI - IMPROVED COLORS FOR BETTER VISIBILITY GM_addStyle(` #reddit-monitor-panel { position: fixed; bottom: 20px; right: 20px; width: 350px; background-color: #1a1a1a; border: 1px solid #444; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.4); z-index: 9999; font-family: Arial, sans-serif; max-height: 500px; overflow: hidden; color: #e0e0e0; } #reddit-monitor-panel.collapsed { width: 200px; height: 40px; overflow: hidden; } #reddit-monitor-header { background-color: #ff4500; color: white; padding: 10px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; font-weight: bold; } #reddit-monitor-content { padding: 15px; max-height: 450px; overflow-y: auto; background-color: #2a2a2a; } .tab-buttons { display: flex; margin-bottom: 15px; border-bottom: 1px solid #444; flex-wrap: wrap; } .tab-button { padding: 8px 15px; cursor: pointer; background: #444; color: #e0e0e0; border: none; outline: none; margin-right: 2px; margin-bottom: 2px; border-radius: 4px 4px 0 0; } .tab-button:hover { background: #555; } .tab-button.active { background: #ff4500; color: white; font-weight: bold; } .tab-content { display: none; margin-top: 10px; } .tab-content.active { display: block; } .input-group { margin-bottom: 15px; } .input-group label { display: block; margin-bottom: 5px; font-weight: bold; color: #e0e0e0; } .input-group input, .input-group select { width: 100%; padding: 8px; border: 1px solid #555; border-radius: 4px; box-sizing: border-box; margin-bottom: 5px; background-color: #333; color: #e0e0e0; } .input-group input:focus, .input-group select:focus { border-color: #ff4500; outline: none; } .monitor-list { margin-top: 15px; max-height: 200px; overflow-y: auto; border: 1px solid #444; padding: 5px; background-color: #333; border-radius: 4px; } .list-item { display: flex; justify-content: space-between; padding: 8px; border-bottom: 1px solid #444; align-items: center; } .list-item:last-child { border-bottom: none; } .list-item button { background: none; border: none; color: #ff7555; cursor: pointer; font-size: 16px; font-weight: bold; padding: 0 5px; } .list-item button:hover { color: #ff4500; } .save-settings, .add-item-btn { background-color: #ff4500; color: white; padding: 8px 12px; border: none; border-radius: 4px; cursor: pointer; margin-top: 5px; display: inline-block; width: auto; font-weight: bold; } .save-settings:hover, .add-item-btn:hover { background-color: #ff6a33; } .status-toggle { margin-top: 15px; display: flex; align-items: center; color: #e0e0e0; } .status-toggle-switch { position: relative; display: inline-block; width: 50px; height: 24px; margin-left: 10px; } .status-toggle-switch input { opacity: 0; width: 0; height: 0; } .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; transition: .4s; border-radius: 24px; } .toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .toggle-slider { background-color: #5cb85c; } input:checked + .toggle-slider:before { transform: translateX(26px); } .notification-log { max-height: 200px; overflow-y: auto; border: 1px solid #444; padding: 10px; margin-top: 10px; background-color: #333; border-radius: 4px; } .notification-item { padding: 8px 0; border-bottom: 1px solid #444; font-size: 13px; } .notification-item:last-child { border-bottom: none; } .notification-item a { color: #4da3ff; text-decoration: none; } .notification-item a:hover { text-decoration: underline; color: #7ab9ff; } .notification-item .time { color: #aaa; font-size: 11px; margin-top: 3px; } `); // Create log storage if it doesn't exist let notificationLog = GM_getValue('notificationLog', []); // Create the UI panel function createUI() { // Remove any existing panel first (for updates/reloads) const existingPanel = document.getElementById('reddit-monitor-panel'); if (existingPanel) { existingPanel.remove(); } const panel = document.createElement('div'); panel.id = 'reddit-monitor-panel'; // Create header const header = document.createElement('div'); header.id = 'reddit-monitor-header'; header.innerHTML = `<span>Reddit Activity Monitor</span><span id="panel-toggle">−</span>`; // Create content area as a div (not innerHTML) const content = document.createElement('div'); content.id = 'reddit-monitor-content'; // Create tabs container const tabButtons = document.createElement('div'); tabButtons.className = 'tab-buttons'; // Create individual tab buttons const tabs = ['settings', 'subreddits', 'users', 'threads', 'log']; tabs.forEach(tab => { const button = document.createElement('button'); button.className = 'tab-button'; if (tab === 'settings') button.classList.add('active'); button.setAttribute('data-tab', tab); button.textContent = tab.charAt(0).toUpperCase() + tab.slice(1); button.addEventListener('click', (e) => { // Update active button document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); e.target.classList.add('active'); // Update active tab document.querySelectorAll('.tab-content').forEach(tabContent => tabContent.classList.remove('active')); document.getElementById(`${tab}-tab`).classList.add('active'); }); tabButtons.appendChild(button); }); // Add tab buttons to content content.appendChild(tabButtons); // Create Settings Tab const settingsTab = document.createElement('div'); settingsTab.className = 'tab-content active'; settingsTab.id = 'settings-tab'; settingsTab.innerHTML = ` <div class="input-group"> <label for="refresh-interval">Refresh Interval (seconds)</label> <input type="number" id="refresh-interval" min="30" value="${CONFIG.refreshInterval/1000}"> </div> <div class="input-group"> <label for="max-notifications">Max Notifications Per Check</label> <input type="number" id="max-notifications" min="1" max="20" value="${CONFIG.maxNotifications}"> </div> <div class="status-toggle"> <span>Monitor Status:</span> <label class="status-toggle-switch"> <input type="checkbox" id="monitor-enabled" ${CONFIG.enabled ? 'checked' : ''}> <span class="toggle-slider"></span> </label> </div> <button class="save-settings" id="save-settings-btn">Save Settings</button> `; // Create Subreddits Tab const subredditsTab = document.createElement('div'); subredditsTab.className = 'tab-content'; subredditsTab.id = 'subreddits-tab'; subredditsTab.innerHTML = ` <div class="input-group"> <label for="new-subreddit">Add Subreddit (without r/)</label> <input type="text" id="new-subreddit" placeholder="Enter subreddit name"> <button class="add-item-btn" id="add-subreddit-btn">Add</button> </div> <div class="monitor-list" id="subreddit-list"> ${generateListHTML(CONFIG.monitorSubreddits, 'subreddit')} </div> `; // Create Users Tab const usersTab = document.createElement('div'); usersTab.className = 'tab-content'; usersTab.id = 'users-tab'; usersTab.innerHTML = ` <div class="input-group"> <label for="new-user">Add User (without u/)</label> <input type="text" id="new-user" placeholder="Enter username"> <button class="add-item-btn" id="add-user-btn">Add</button> </div> <div class="monitor-list" id="user-list"> ${generateListHTML(CONFIG.monitorUsers, 'user')} </div> `; // Create Threads Tab const threadsTab = document.createElement('div'); threadsTab.className = 'tab-content'; threadsTab.id = 'threads-tab'; threadsTab.innerHTML = ` <div class="input-group"> <label for="new-thread">Add Thread ID or URL</label> <input type="text" id="new-thread" placeholder="Enter thread ID or Reddit URL"> <button class="add-item-btn" id="add-thread-btn">Add</button> </div> <div class="monitor-list" id="thread-list"> ${generateListHTML(CONFIG.monitorThreads, 'thread')} </div> `; // Create Log Tab const logTab = document.createElement('div'); logTab.className = 'tab-content'; logTab.id = 'log-tab'; logTab.innerHTML = ` <h3>Recent Notifications</h3> <div class="notification-log" id="notification-log"> ${generateLogHTML()} </div> <button class="save-settings" id="clear-log-btn">Clear Log</button> `; // Add tabs to content content.appendChild(settingsTab); content.appendChild(subredditsTab); content.appendChild(usersTab); content.appendChild(threadsTab); content.appendChild(logTab); // Add elements to panel panel.appendChild(header); panel.appendChild(content); // Add panel to page document.body.appendChild(panel); // Header click event listener header.addEventListener('click', function(e) { // Only toggle if clicking on the header itself or the toggle button if (e.target === header || e.target.id === 'panel-toggle') { togglePanel(); } }); // Settings save button document.getElementById('save-settings-btn').addEventListener('click', saveSettings); // Add item buttons document.getElementById('add-subreddit-btn').addEventListener('click', () => addItem('subreddit')); document.getElementById('add-user-btn').addEventListener('click', () => addItem('user')); document.getElementById('add-thread-btn').addEventListener('click', () => addItem('thread')); // Add event listeners to remove buttons addRemoveButtonListeners(); // Clear log button document.getElementById('clear-log-btn').addEventListener('click', clearLog); // Monitor toggle document.getElementById('monitor-enabled').addEventListener('change', toggleMonitor); } // Add event listeners to all remove buttons function addRemoveButtonListeners() { document.querySelectorAll('.remove-item-btn').forEach(button => { button.addEventListener('click', (e) => { const type = e.target.getAttribute('data-type'); const value = e.target.getAttribute('data-value'); removeItem(type, value); }); }); } // Helper function to generate list HTML function generateListHTML(items, type) { if (!items || items.length === 0) { return '<p>No items added yet.</p>'; } let html = ''; items.forEach(item => { html += ` <div class="list-item"> <span>${item}</span> <button class="remove-item-btn" data-type="${type}" data-value="${item}">×</button> </div> `; }); return html; } // Helper function to generate log HTML function generateLogHTML() { if (!notificationLog || notificationLog.length === 0) { return '<p>No notifications yet.</p>'; } let html = ''; notificationLog.slice(0, 50).forEach(item => { html += ` <div class="notification-item"> <div><a href="${item.url}" target="_blank">${item.title}</a></div> <div>${item.text}</div> <div class="time">${new Date(item.time).toLocaleString()}</div> </div> `; }); return html; } // Toggle panel expanded/collapsed function togglePanel() { const panel = document.getElementById('reddit-monitor-panel'); const toggle = document.getElementById('panel-toggle'); if (panel.classList.contains('collapsed')) { panel.classList.remove('collapsed'); toggle.textContent = '−'; } else { panel.classList.add('collapsed'); toggle.textContent = '+'; } } // Save settings function saveSettings() { const refreshIntervalInput = document.getElementById('refresh-interval'); const maxNotificationsInput = document.getElementById('max-notifications'); // Validate inputs if (!refreshIntervalInput.value || isNaN(refreshIntervalInput.value) || !maxNotificationsInput.value || isNaN(maxNotificationsInput.value)) { alert('Please enter valid numbers for the settings.'); return; } const refreshInterval = Math.max(30, parseInt(refreshIntervalInput.value)) * 1000; const maxNotifications = Math.min(20, Math.max(1, parseInt(maxNotificationsInput.value))); CONFIG.refreshInterval = refreshInterval; CONFIG.maxNotifications = maxNotifications; saveConfig(); // Restart the monitor with new settings if (CONFIG.enabled) { stopMonitoring(); startMonitoring(); } alert('Settings saved!'); } // Add item to a list function addItem(type) { const input = document.getElementById(`new-${type}`); let value = input.value.trim(); if (!value) { alert(`Please enter a ${type} to add.`); return; } // Process thread URLs to extract ID if (type === 'thread' && value.includes('reddit.com')) { const match = value.match(/reddit\.com\/r\/[^/]+\/comments\/([^/]+)/); if (match && match[1]) { value = match[1]; } } // Determine which config array to update let targetArray; switch(type) { case 'subreddit': // Remove r/ prefix if present value = value.replace(/^r\//, '').toLowerCase(); targetArray = CONFIG.monitorSubreddits; break; case 'user': // Remove u/ prefix if present value = value.replace(/^u\//, ''); targetArray = CONFIG.monitorUsers; break; case 'thread': // Add t3_ prefix if not present value = value.startsWith('t3_') ? value : `t3_${value}`; targetArray = CONFIG.monitorThreads; break; } // Check if item already exists if (targetArray.includes(value)) { alert(`This ${type} is already being monitored.`); return; } // Add item targetArray.push(value); // Update UI document.getElementById(`${type}-list`).innerHTML = generateListHTML(targetArray, type); // Re-add event listeners for remove buttons addRemoveButtonListeners(); // Clear input input.value = ''; // Save config saveConfig(); } // Remove item from a list function removeItem(type, value) { let targetArray; switch(type) { case 'subreddit': targetArray = CONFIG.monitorSubreddits; break; case 'user': targetArray = CONFIG.monitorUsers; break; case 'thread': targetArray = CONFIG.monitorThreads; break; } // Find and remove item const index = targetArray.indexOf(value); if (index !== -1) { targetArray.splice(index, 1); } // Update UI document.getElementById(`${type}-list`).innerHTML = generateListHTML(targetArray, type); // Re-add event listeners for remove buttons addRemoveButtonListeners(); // Save config saveConfig(); } // Clear notification log function clearLog() { notificationLog = []; GM_setValue('notificationLog', notificationLog); document.getElementById('notification-log').innerHTML = '<p>No notifications yet.</p>'; } // Toggle monitoring on/off function toggleMonitor() { CONFIG.enabled = document.getElementById('monitor-enabled').checked; saveConfig(); if (CONFIG.enabled) { startMonitoring(); } else { stopMonitoring(); } } // Save config to GM storage function saveConfig() { GM_setValue('redditMonitorConfig', CONFIG); } // Monitor timer reference let monitorTimer = null; // Start the monitoring process function startMonitoring() { if (monitorTimer) { clearTimeout(monitorTimer); } checkForNewContent(); } // Stop the monitoring process function stopMonitoring() { if (monitorTimer) { clearTimeout(monitorTimer); monitorTimer = null; } } // Main function to periodically check for new content function checkForNewContent() { if (!CONFIG.enabled) return; // Check subreddits for new posts CONFIG.monitorSubreddits.forEach(sub => { fetchSubredditPosts(sub); }); // Check user activity CONFIG.monitorUsers.forEach(user => { fetchUserActivity(user); }); // Check specific threads for new comments CONFIG.monitorThreads.forEach(threadId => { fetchThreadComments(threadId); }); // Schedule next check monitorTimer = setTimeout(checkForNewContent, CONFIG.refreshInterval); } // Example function to fetch subreddit posts using Reddit JSON API function fetchSubredditPosts(subreddit) { GM_xmlhttpRequest({ method: "GET", url: `https://www.reddit.com/r/${subreddit}/new.json?limit=10`, onload: function(response) { try { const data = JSON.parse(response.responseText); const posts = data.data.children; let newPosts = []; for(const post of posts) { const postId = post.data.id; if(!seenContent.posts[postId]) { seenContent.posts[postId] = true; newPosts.push(post.data); } } // Show notifications for new posts (limited by maxNotifications) newPosts.slice(0, CONFIG.maxNotifications).forEach(post => { const title = `New post in r/${subreddit}`; const text = `${post.title.substring(0, 50)}${post.title.length > 50 ? '...' : ''}`; const url = `https://www.reddit.com${post.permalink}`; showNotification(title, text, url); // Add to log addToLog(title, text, url); }); // Save updated seen content GM_setValue('seenContent', seenContent); // Update log in UI if log tab is open updateLogUI(); } catch(e) { console.error("Error processing subreddit posts:", e); } } }); } // Function to fetch user activity function fetchUserActivity(username) { GM_xmlhttpRequest({ method: "GET", url: `https://www.reddit.com/user/${username}/overview.json?limit=10`, onload: function(response) { try { const data = JSON.parse(response.responseText); const activities = data.data.children; let newActivities = []; for(const activity of activities) { const activityId = activity.data.id; const activityType = activity.data.hasOwnProperty('link_title') ? 'comment' : 'post'; const storageKey = activityType + 's'; if(!seenContent[storageKey]) { seenContent[storageKey] = {}; } if(!seenContent[storageKey][activityId]) { seenContent[storageKey][activityId] = true; newActivities.push({data: activity.data, type: activityType}); } } // Show notifications for new activities newActivities.slice(0, CONFIG.maxNotifications).forEach(activity => { const isComment = activity.type === 'comment'; const title = `New ${isComment ? 'comment' : 'post'} by u/${username}`; const text = isComment ? `On post: ${activity.data.link_title.substring(0, 40)}...` : `${activity.data.title.substring(0, 50)}${activity.data.title.length > 50 ? '...' : ''}`; const url = `https://www.reddit.com${activity.data.permalink}`; showNotification(title, text, url); // Add to log addToLog(title, text, url); }); // Save updated seen content GM_setValue('seenContent', seenContent); // Update log in UI if log tab is open updateLogUI(); } catch(e) { console.error("Error processing user activity:", e); } } }); } // Function to fetch thread comments function fetchThreadComments(threadId) { // Extract the actual ID if it starts with t3_ const id = threadId.startsWith('t3_') ? threadId.substring(3) : threadId; GM_xmlhttpRequest({ method: "GET", url: `https://www.reddit.com/comments/${id}.json`, onload: function(response) { try { const data = JSON.parse(response.responseText); // The second element of the array contains the comments const comments = data[1].data.children; const newComments = []; processComments(comments, newComments, threadId, data[0].data.children[0].data.title); // Save updated seen content GM_setValue('seenContent', seenContent); // Update log in UI if log tab is open updateLogUI(); } catch(e) { console.error("Error processing thread comments:", e); } } }); } // Recursive function to process comments including replies function processComments(comments, newComments, threadId, threadTitle) { for(const comment of comments) { // Skip non-comment items like "more" links if(comment.kind !== 't1') continue; const commentId = comment.data.id; if(!seenContent.comments) { seenContent.comments = {}; } if(!seenContent.comments[commentId]) { seenContent.comments[commentId] = true; newComments.push(comment.data); // Limit notifications if(newComments.length <= CONFIG.maxNotifications) { const title = `New comment in thread`; const text = `u/${comment.data.author} on "${threadTitle.substring(0, 30)}...": ${comment.data.body.substring(0, 40)}${comment.data.body.length > 40 ? '...' : ''}`; const url = `https://www.reddit.com/comments/${threadId.replace('t3_', '')}/comment/${commentId}/`; showNotification(title, text, url); // Add to log addToLog(title, text, url); } } // Process replies if any if(comment.data.replies && comment.data.replies.data && comment.data.replies.data.children) { processComments(comment.data.replies.data.children, newComments, threadId, threadTitle); } } } // Function to show a notification function showNotification(title, text, url) { GM_notification({ title: title, text: text, timeout: 10000, onclick: function() { window.open(url, '_blank'); } }); } // Add to notification log function addToLog(title, text, url) { notificationLog.unshift({ title: title, text: text, url: url, time: Date.now() }); // Limit log size if (notificationLog.length > 100) { notificationLog = notificationLog.slice(0, 100); } GM_setValue('notificationLog', notificationLog); } // Update log UI if visible function updateLogUI() { const logElement = document.getElementById('notification-log'); if (logElement) { logElement.innerHTML = generateLogHTML(); } } // Initialize - wait for page to fully load window.addEventListener('load', function() { // Short delay to ensure Reddit's UI is fully loaded setTimeout(function() { // Create UI createUI(); // Start monitoring if enabled if (CONFIG.enabled) { startMonitoring(); } }, 1000); });