// ==UserScript== // @name linuxdo保活 // @namespace http://tampermonkey.net/ // @version 0.2.4.2 // @description linuxdo自动浏览帖子,自动点赞 // @author oxzk // @match https://linux.do/* // @grant GM_setValue // @grant GM_getValue // @license MIT // @homepageURL https://greasyfork.org/zh-CN/scripts/560774-linuxdo%E4%BF%9D%E6%B4%BB // @downloadURL none // ==/UserScript== ;(function () { 'use strict' // ==================== 配置 ==================== const CONFIG = { scroll: { interval: 1200, // 滚动间隔(毫秒) step: 600, // 每次滚动像素 duration: 30, // 滚动持续时间(秒) }, limits: { viewThreshold: 1, // 浏览量阈值,超过才点赞 maxTopics: 300, // 最大浏览帖子数 maxRunTime: 360, // 最大运行时间(分钟) browseTimeout: 60000, // 单帖浏览超时(毫秒) }, urls: { base: 'https://linux.do/new', }, iframe: { width: '50%', height: '100%', top: '0px', left: '0px', position: 'fixed', zIndex: '9999', }, logging: { enabled: true, levels: { error: true, warn: true, info: true, debug: false }, }, storage: { stats: 'linuxdoStats', enabled: 'linuxdoHelperEnabled', }, } // ==================== 工具函数 ==================== const utils = { // 延时函数 sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), // 随机延时 randomSleep: (maxMs) => utils.sleep(Math.floor(Math.random() * maxMs) + 1000), // Promise 超时包装 withTimeout: (promise, ms) => Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error('操作超时')), ms))]), // 格式化时间 formatDuration: (seconds) => { const h = Math.floor(seconds / 3600) const m = Math.floor((seconds % 3600) / 60) const s = seconds % 60 return `${h}时${m}分${s}秒` }, // 解析浏览量 parseViewCount: (text) => { const match = text?.match(/此话题已被浏览\s*([\d,]+)\s*次/) return match ? parseInt(match[1].replace(/,/g, '')) : 0 }, // 数组随机打乱 shuffle: (arr) => [...arr].sort(() => Math.random() - 0.5), } // ==================== 日志模块 ==================== const logger = { _log: (level, ...args) => { if (CONFIG.logging.enabled && CONFIG.logging.levels[level]) { const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : level === 'debug' ? 'debug' : 'log' console[method](`[LinuxDo助手]`, ...args) } }, error: (...args) => logger._log('error', '❌', ...args), warn: (...args) => logger._log('warn', '⚠️', ...args), info: (...args) => logger._log('info', ...args), debug: (...args) => logger._log('debug', '🔍', ...args), } // ==================== 统计模块 ==================== const stats = { totalViews: 0, totalLikes: 0, sessionViews: 0, sessionLikes: 0, startTime: Date.now(), load() { const saved = GM_getValue(CONFIG.storage.stats, {}) this.totalViews = saved.totalViews || 0 this.totalLikes = saved.totalLikes || 0 logger.info('📊 历史统计 - 浏览:', this.totalViews, '点赞:', this.totalLikes) }, save() { GM_setValue(CONFIG.storage.stats, { totalViews: this.totalViews, totalLikes: this.totalLikes, }) }, addView() { this.sessionViews++ this.totalViews++ this.save() }, addLike() { this.sessionLikes++ this.totalLikes++ this.save() }, getRunTime() { return Math.floor((Date.now() - this.startTime) / 1000) }, print() { logger.info('\n📊 统计信息') logger.info('-------------------') logger.info(`🕒 运行时间:${utils.formatDuration(this.getRunTime())}`) logger.info(`👀 本次浏览:${this.sessionViews}帖`) logger.info(`❤️ 本次点赞:${this.sessionLikes}次`) logger.info(`📈 总浏览数:${this.totalViews}帖`) logger.info(`💖 总点赞数:${this.totalLikes}次`) logger.info('-------------------\n') }, } // ==================== 开关控制 ==================== const switchControl = { get enabled() { return GM_getValue(CONFIG.storage.enabled, false) }, set enabled(value) { GM_setValue(CONFIG.storage.enabled, value) }, toggle() { // 清理本次会话数据 stats.sessionViews = 0 stats.sessionLikes = 0 stats.startTime = Date.now() core.likeLimited = false const newState = !this.enabled this.enabled = newState logger.info(`助手已${newState ? '启用' : '禁用'}`) if (newState) { window.location.href = CONFIG.urls.base } return newState }, } // ==================== UI 模块 ==================== const ui = { link: null, use: null, updateIcon(enabled) { if (this.use) { this.use.setAttribute('href', enabled ? '#pause' : '#play') } if (this.link) { this.link.title = enabled ? '停止助手' : '启动助手' this.link.classList.toggle('active', enabled) } }, createSwitchIcon() { const container = document.getElementById('toggle-current-user')?.parentElement if (!container) { logger.error('未找到导航栏容器') return } const li = document.createElement('li') li.className = 'header-dropdown-toggle linux-do-tools' this.link = document.createElement('a') this.link.href = 'javascript:void(0)' this.link.className = 'btn no-text icon btn-flat' this.link.tabIndex = 0 const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') svg.setAttribute('class', 'fa d-icon svg-icon prefix-icon svg-string') this.use = document.createElementNS('http://www.w3.org/2000/svg', 'use') this.updateIcon(switchControl.enabled) svg.appendChild(this.use) this.link.appendChild(svg) li.appendChild(this.link) this.link.addEventListener('click', () => { this.updateIcon(switchControl.toggle()) }) container.parentNode.insertBefore(li, container.nextSibling) }, } // ==================== 核心功能 ==================== const core = { currentIframe: null, popstateHandler: null, likeLimited: false, // 429 限制标志 // 检查是否应停止 shouldStop() { if (stats.sessionViews >= CONFIG.limits.maxTopics) { logger.info(`🛑 已达最大浏览数 ${CONFIG.limits.maxTopics}`) return true } if (stats.getRunTime() >= CONFIG.limits.maxRunTime * 60) { logger.info(`🛑 已达最大运行时间 ${CONFIG.limits.maxRunTime}分钟`) return true } return false }, // 停止脚本 stop() { switchControl.enabled = false stats.print() logger.info('✨ 脚本已停止') }, // 获取帖子列表 getTopics() { const topics = [] const elements = document.querySelectorAll('#list-area .title') elements.forEach((el) => { const row = el.closest('tr') if (!row || row.querySelector('.topic-statuses .pinned')) return const viewsEl = row.querySelector('.num.views .number') const viewsTitle = viewsEl?.getAttribute('title') || '' topics.push({ title: el.textContent.trim(), url: el.href, views: utils.parseViewCount(viewsTitle), }) }) logger.info(`📋 找到 ${topics.length} 个帖子`) return topics }, // 点赞操作 async likePost(targetWindow) { // 如果已被限制,跳过点赞 if (this.likeLimited) { logger.warn('点赞已被限制(429),跳过') return } try { const buttons = targetWindow.document.querySelectorAll('button.btn-toggle-reaction-like') let likeCount = 0 const maxLikes = 3 for (const btn of buttons) { if (likeCount >= maxLikes) break if (this.likeLimited) break if (!btn.title.includes('点赞此帖子')) { logger.debug('已点赞,跳过') continue } // 获取帖子 ID const postContainer = btn.closest('article[data-post-id]') const postId = postContainer?.dataset?.postId if (!postId) { logger.debug('未找到帖子ID,跳过') continue } // 调用点赞接口 try { // 获取 CSRF token const csrfToken = targetWindow.document.querySelector('meta[name="csrf-token"]')?.content if (!csrfToken) { logger.warn('未找到 CSRF token,跳过点赞') continue } const response = await fetch( `https://linux.do/discourse-reactions/posts/${postId}/custom-reactions/heart/toggle.json`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': csrfToken, }, credentials: 'include', } ) if (response.status === 429) { logger.warn('🚫 点赞频率限制(429),后续不再点赞') this.likeLimited = true return } if (response.ok) { stats.addLike() likeCount++ logger.info('👍 点赞成功') } else { logger.warn(`点赞失败: ${response.status}`) } } catch (fetchError) { logger.error('点赞请求失败:', fetchError.message) } // 点赞间隔,避免过快 await utils.sleep(1000) } } catch (e) { logger.error('点赞失败:', e.message) } }, // 浏览单个帖子 async browseTopic(topic) { logger.info(`📖 浏览: ${topic.title}`) stats.addView() const iframe = document.createElement('iframe') Object.assign(iframe.style, CONFIG.iframe) iframe.src = `${topic.url}?_t=${Date.now()}` // 清理旧的 iframe if (this.currentIframe) { this.currentIframe.remove() } this.currentIframe = iframe // 防止 history 污染(只添加一次) if (!this.popstateHandler) { this.popstateHandler = (e) => e.stopPropagation() window.addEventListener('popstate', this.popstateHandler, true) } document.body.appendChild(iframe) // 等待加载 await new Promise((resolve) => { iframe.onload = resolve }) // 点赞 await this.likePost(iframe.contentWindow) // 滚动浏览 await this.scrollIframe(iframe) // 清理 iframe.remove() this.currentIframe = null stats.print() }, // iframe 滚动 async scrollIframe(iframe) { return new Promise((resolve) => { const startTime = Date.now() const { interval, step, duration } = CONFIG.scroll const timer = setInterval(() => { try { const win = iframe.contentWindow const doc = win.document.documentElement const atBottom = win.scrollY + win.innerHeight + 1 >= doc.scrollHeight const timeout = Date.now() - startTime >= duration * 1000 if (atBottom || timeout) { clearInterval(timer) resolve() return } win.scrollBy(0, step) } catch (e) { clearInterval(timer) resolve() } }, interval) }) }, // 主浏览循环 async browseLoop() { try { const topics = utils.shuffle(this.getTopics()) for (const topic of topics) { if (this.shouldStop()) { this.stop() return } if (!switchControl.enabled) { logger.info('⏹️ 用户停止') return } try { await utils.withTimeout(this.browseTopic(topic), CONFIG.limits.browseTimeout) } catch (e) { logger.warn(`帖子浏览超时,跳过: ${topic.title}`) } await utils.randomSleep(3000) } // 继续下一轮 if (!this.shouldStop() && switchControl.enabled) { ui.updateIcon(switchControl.toggle()) this.stop() logger.info('📄 当前页面完成,停止10秒后重新启动...') await utils.sleep(10000) logger.info('🔄 已清理历史数据,重新开始浏览...') ui.updateIcon(switchControl.toggle()) // 重新启用,跳转到 base URL } } catch (e) { logger.error('浏览出错:', e.message) } }, } // ==================== 入口 ==================== async function main() { ui.createSwitchIcon() if (!switchControl.enabled) return stats.load() if (window.location.href.includes(CONFIG.urls.base)) { if (core.shouldStop()) { core.stop() return } await core.browseLoop() } } // 启动 if (document.readyState === 'complete') { main() } else { window.addEventListener('load', main) } })()