V2ex Random Floor

祝你好运

  1. // ==UserScript==
  2. // @name V2ex Random Floor
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-05-28
  5. // @description 祝你好运
  6. // @author You
  7. // @match https://www.v2ex.com/t/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=v2ex.com
  9. // @grant GM_registerMenuCommand
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.js
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. const V2exMaxPageSize = 20
  18.  
  19. // ==UserScript菜单==
  20. if (typeof GM_registerMenuCommand !== 'undefined') {
  21. GM_registerMenuCommand('启动抽奖', async function() {
  22. const urlTopicId = window.location.pathname.match(/\/t\/(\d+)/)[1];
  23. const topicId = prompt('请输入主题ID:', urlTopicId || '');
  24. if (!topicId) return alert('主题ID不能为空');
  25.  
  26. const defaultToken = window.localStorage.getItem('v2ex-random-floor-token') || '';
  27. const userToken = prompt('请输入用户Token,在设置 - Tokens 中生成:', defaultToken);
  28. if (!userToken) return alert('用户Token不能为空');
  29. window.localStorage.setItem('v2ex-random-floor-token', userToken);
  30.  
  31. const luckyCount = parseInt(prompt('请输入抽奖人数:', '1'), 10) || 1;
  32. if (isNaN(luckyCount) || luckyCount < 1) return alert('抽奖人数必须大于0');
  33.  
  34. const isUnique = confirm('是否用户去重? (确定=是, 取消=否)');
  35. const today = new Date();
  36. const endTime = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1, 0, 0, 0);
  37. const deadline = prompt('请输入截止时间(可留空, 格式: yyyy-mm-dd HH:MM):', `${endTime.toISOString().slice(0, 10)} 24:00`);
  38.  
  39. if (new Date(deadline).getTime() < 0) return alert('截止时间格式不正确');
  40.  
  41. const maxUserCount = parseInt(prompt('请输入最大参与人次(可留空):', '0'), 10) || 0;
  42. if (isNaN(maxUserCount) || maxUserCount < 0) return alert('最大参与人次必须大于等于0');
  43. if (maxUserCount && maxUserCount < luckyCount) return alert('最大参与人次必须大于等于抽奖人数');
  44.  
  45. const randomSeed = prompt('请输入随机种子,如总楼层数,可引入外部随机变量保证公平性:', '');
  46. const options = { topicId, luckyCount, isUnique, deadline, randomSeed, userToken, maxUserCount };
  47.  
  48.  
  49. const rf = new RandomFloor(options);
  50. try {
  51. console.log(`开始抽奖: 主题ID=${rf.topicId}, 抽奖人数=${rf.luckyCount}, 用户去重=${rf.isUnique}, 截止时间=${rf.deadline}, 最大参与人次=${rf.maxUserCount}`);
  52. await rf.run();
  53. alert('抽奖执行完毕, 结果请查看控制台日志');
  54. } catch (e) {
  55. alert('抽奖出错: ' + e.message);
  56. }
  57. });
  58. }
  59.  
  60. class RandomFloor {
  61. constructor(options) {
  62. // 主题 ID
  63. this.topicId = options.topicId
  64. // 随机种子
  65. this.nextRandomSeed = new Math.seedrandom(options.randomSeed || Math.random())
  66. // 抽奖人数
  67. this.luckyCount = options.luckyCount || 1
  68. // 是否用户去重
  69. this.isUnique = options.isUnique || false
  70. // 截止时间
  71. this.deadline = options.deadline || 0
  72. // 最多参与人次
  73. this.maxUserCount = options.maxUserCount || 0
  74. // 回帖列表
  75. this.replies = []
  76. // token
  77. this.token = options.userToken || ''
  78. }
  79.  
  80. async run() {
  81. const replies = await this.getReplyList(this.topicId)
  82. const validateList = this.filterReplies(replies)
  83. this.addLog(`累计 ${replies.length} 条回帖,去重后 ${validateList.length} 条`)
  84. if (validateList.length < this.luckyCount) {
  85. this.addLog(`回帖数量不足,无法抽奖`)
  86. return
  87. }
  88. this.addLog(`开始抽奖...`)
  89. const nextId = this.nextRandomSeed
  90. const luckyList = []
  91. const luckySet = new Set()
  92. while (luckyList.length < this.luckyCount) {
  93. const index = Math.floor(nextId() * validateList.length)
  94. const item = validateList[index]
  95. if (this.isUnique && luckySet.has(item.userId)) {
  96. continue
  97. }
  98. luckySet.add(item.userId)
  99. luckyList.push(item)
  100. }
  101. this.addLog(`抽奖完成,中奖名单如下:`)
  102.  
  103. let messageText = `抽奖完成,中奖名单如下:\n`
  104. luckyList.forEach(item => {
  105. messageText += `第${item.index}楼 @${item.userName} (${item.userId})\n`
  106. })
  107. this.addLog(messageText)
  108. this.addLog(`抽奖结束`)
  109. }
  110.  
  111. get deadlineStamp() {
  112. if (typeof this.deadline === 'number') {
  113. return this.deadline
  114. }
  115. if (typeof this.deadline === 'string') {
  116. return new Date(this.deadline).getTime()
  117. }
  118. if (this.deadline instanceof Date) {
  119. return this.deadline.getTime()
  120. }
  121. return 0
  122. }
  123.  
  124. filterReplies(replies) {
  125. const { isUnique, deadlineStamp, maxUserCount } = this
  126. const userIds = new Set()
  127. const items = replies.filter(item => {
  128. if (isUnique && userIds.has(item.userId)) {
  129. return false
  130. }
  131. if (deadlineStamp && item.created > deadlineStamp) {
  132. return false
  133. }
  134. userIds.add(item.userId)
  135. return true
  136. })
  137. return maxUserCount > 0 ? items.slice(0, maxUserCount) : items
  138. }
  139.  
  140. async getReplyList(topicId, page = 1) {
  141. this.addLog(`获取楼层列表: ${page}页 获取中...`)
  142. const replies = await fetch(`/api/v2/topics/${topicId}/replies?p=${page}`, {
  143. method: 'GET',
  144. headers: { Authorization: 'Bearer ' + this.token }
  145. }).then(res => {
  146. if (res.status !== 200) {
  147. throw new Error(`获取楼层列表失败: ${res.status} ${res.statusText}`)
  148. }
  149. return res.json()
  150. })
  151. if (!replies.success) {
  152. throw new Error(`获取楼层列表失败: ${replies.message}`)
  153. }
  154. if (!replies.result.length) {
  155. return this.replies
  156. }
  157. const dataList = replies.result.map((item, index) => {
  158. return {
  159. index: this.replies.length + index + 1,
  160. userId: item.member.id,
  161. userName: item.member.username,
  162. created: item.created * 1000,
  163. }
  164. })
  165. this.replies.push(...dataList)
  166. this.addLog(`获取楼层列表: ${page}页 获取到 ${dataList.length} 条`)
  167. if (dataList.length < V2exMaxPageSize) {
  168. this.addLog(`获取楼层列表: ${page}页 获取完毕`)
  169. return this.replies
  170. }
  171. return this.getReplyList(topicId, page + 1)
  172. }
  173.  
  174. addLog(message) {
  175. console.info(`[v2ex RandomFloor] ${message}`)
  176. }
  177. }
  178.  
  179. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址