长毛象抽奖脚本

点击“开始抽奖”后,随机抽出五名中奖候选者。

  1. // ==UserScript==
  2. // @name 长毛象抽奖脚本
  3. // @namespace https://blog.bgme.me
  4. // @match https://bgme.me/*
  5. // @match https://bgme.bid/*
  6. // @match https://c.bgme.bid/*
  7. // @grant none
  8. // @run-at document-end
  9. // @version 1.0.0
  10. // @author bgme
  11. // @description 点击“开始抽奖”后,随机抽出五名中奖候选者。
  12. // @supportURL https://github.com/yingziwu/Greasemonkey/issues
  13. // @license AGPL-3.0-or-later
  14. // ==/UserScript==
  15.  
  16. window.addEventListener('load', function () {
  17. activateMastodonLottery();
  18. }, false)
  19.  
  20. function chromeClickChecker(event) {
  21. return (
  22. event.target.tagName.toLowerCase() === 'i' &&
  23. event.target.classList.contains('fa-ellipsis-h') &&
  24. document.querySelector('div.dropdown-menu') === null
  25. );
  26. }
  27.  
  28. function firefoxClickChecker(event) {
  29. return (
  30. event.target.tagName.toLowerCase() === 'button' &&
  31. event.target.classList.contains('icon-button') &&
  32. document.querySelector('div.dropdown-menu') === null
  33. );
  34. }
  35.  
  36. function activateMastodonLottery() {
  37. document.querySelector('body').addEventListener('click', function (event) {
  38. if (chromeClickChecker(event) || firefoxClickChecker(event)) {
  39. // Get the status for this event
  40. let status = event.target.parentNode.parentNode.parentNode.parentNode.parentNode;
  41. if (status.className.match('detailed-status__wrapper')) {
  42. addLotteryLink(status);
  43. }
  44. };
  45. }, false);
  46. }
  47.  
  48. function addLotteryLink(status) {
  49. setTimeout(function () {
  50. const lotteryStatusUrl = status.querySelector('.detailed-status__datetime').getAttribute('href');
  51. const dropdown = document.querySelector('div.dropdown-menu ul');
  52. const separator = dropdown.querySelector('li.dropdown-menu__separator');
  53.  
  54. const listItem = document.createElement('li');
  55. listItem.classList.add('dropdown-menu__item');
  56. listItem.classList.add('mastodon__lottery');
  57.  
  58. const link = document.createElement('a');
  59. link.setAttribute('href', '#');
  60. link.setAttribute('target', '_blank');
  61. link.textContent = '开始抽奖';
  62.  
  63. link.addEventListener('click', function (e) {
  64. e.preventDefault();
  65. if (!window.lotteryRunning) {
  66. window.lotteryRunning = true;
  67. link.textContent = '抽奖中,请等待……';
  68. run(lotteryStatusUrl).then(() => { window.lotteryRunning = false }).catch(() => { window.lotteryRunning = false });
  69. }
  70. }, false);
  71.  
  72. listItem.appendChild(link);
  73. dropdown.insertBefore(listItem, separator);
  74. }, 100);
  75. }
  76.  
  77. async function run(lotteryStatusUrl, lotteryType = 'reblog', candidateNumber = 5) {
  78. // lotteryStatusUrl 抽奖嘟文URL
  79. // lotteryType 抽奖类型:转发(reblog),收藏(favourite)
  80. // candidateNumber 候选中奖者人数
  81.  
  82. const domain = document.location.hostname;
  83. const token = JSON.parse(document.querySelector('#initial-state').text).meta.access_token;
  84. const API = {
  85. 'verify': `https://${domain}/api/v1/accounts/verify_credentials`,
  86. 'notifications': `https://${domain}/api/v1/notifications`,
  87. 'status': `https://${domain}/api/v1/statuses/`,
  88. };
  89. const searchParamMap = new Map([
  90. ['reblog', 'exclude_types[]=follow&exclude_types[]=follow_request&exclude_types[]=favourite&exclude_types[]=mention&exclude_types[]=poll'],
  91. ['favourite', 'exclude_types[]=follow&exclude_types[]=follow_request&exclude_types[]=reblog&exclude_types[]=mention&exclude_types[]=poll'],
  92. ]);
  93. const searchParam = new URLSearchParams(searchParamMap.get(lotteryType));
  94.  
  95. const statusID = lotteryStatusUrl.match(/(\d+)$/)[0];
  96. let statusTNumber;
  97. let lotterLog;
  98.  
  99.  
  100. logout(`开始抽奖……\n当前浏览器:${navigator.userAgent}\n开始时间:${(new Date()).toISOString()}`);
  101. logout(`抽奖嘟文:${lotteryStatusUrl},抽奖类型:${lotteryType},候选中奖者人数:${candidateNumber}\n\n`);
  102. let verify;
  103. [verify, statusTNumber] = await doVerify(API, lotteryStatusUrl, statusID, lotteryType, statusTNumber);
  104. if (!verify) {
  105. throw Error('抽奖嘟文非本人发送');
  106. }
  107. const matchAccouts = await getmatchAccouts(API, statusID, statusTNumber, searchParam);
  108. randomTest(matchAccouts);
  109. const luckGuys = getLuckGuy(matchAccouts);
  110. const cadidatesText = getCandidate(luckGuys, candidateNumber);
  111. const notificationText = `嘿!感谢各位参与本次小抽奖活动。\n${cadidatesText}\n\n希望这条艾特您的信息没有造成骚扰,如您对奖品感兴趣请和我私信联系吧?`;
  112. await postStatus(notificationText, statusID, 'public');
  113. logout(`抽奖结束!\n结束时间:${(new Date()).toISOString()}`);
  114. saveFile(lotterLog, `lotterLog-${Date.now()}.log`, 'text/plain; charset=utf-8');
  115.  
  116.  
  117. async function doVerify(API, lotteryStatusUrl, statusID, lotteryType, statusTNumber) {
  118. const v = await request(API.verify);
  119. const s = await request(`${API.status}${statusID}`);
  120. logout(`抽奖嘟文URL${lotteryStatusUrl}\n回复数:${s.replies_count},转发数:${s.reblogs_count},收藏数:${s.favourites_count}`);
  121.  
  122. const numbers = new Map([['reblog', s.reblogs_count], ['favourite', s.favourites_count]]);
  123. if (numbers.has(lotteryType)) {
  124. statusTNumber = numbers.get(lotteryType);
  125. } else {
  126. throw Error('抽奖类型设置不正确');
  127. }
  128.  
  129. if (v.acct === s.account.acct && (new URL(s.account.url)).hostname === (new URL(lotteryStatusUrl)).hostname) {
  130. return [true, statusTNumber];
  131. } else {
  132. return [false, statusTNumber];
  133. }
  134. }
  135.  
  136. async function getmatchAccouts(API, statusID, statusTNumber, searchParam) {
  137. const matchAccouts = [];
  138.  
  139. while (matchAccouts.length !== statusTNumber) {
  140. const nlist = await request(`${API.notifications}?${searchParam.toString()}`);
  141. searchParam.set('max_id', nlist.slice(-1)[0].id);
  142.  
  143. nlist.forEach((obj) => {
  144. if (obj.status.id === statusID) {
  145. matchAccouts.push(obj.account.acct);
  146. }
  147. });
  148. }
  149.  
  150. matchAccouts.sort();
  151. logout(`共有${matchAccouts.length}名符合条件的抽奖参与者\n她们是:`);
  152. matchAccouts.forEach(logout);
  153.  
  154. return matchAccouts;
  155. }
  156.  
  157. function randomTest(matchAccouts) {
  158. logout('随机函数测试:');
  159. const testResults = [];
  160. const n = 20;
  161. for (let i = 0; i < (n * 20); i++) {
  162. testResults.push(getRandomIndex(matchAccouts));
  163. }
  164. for (let i = 0; i < n; i++) {
  165. logout(testResults.slice((i * 20), ((i + 1) * 20)).join(', '));
  166. }
  167. }
  168.  
  169. function getLuckGuy(matchAccouts) {
  170. const luckGuys = [];
  171. const n = matchAccouts.length;
  172. const luckGuysMap = new Map();
  173. for (let i = 0; i < (n * 100); i++) {
  174. const luckGuy = matchAccouts[getRandomIndex(matchAccouts)];
  175. if (luckGuysMap.get(luckGuy)) {
  176. luckGuysMap.set(luckGuy, luckGuysMap.get(luckGuy) + 1);
  177. } else {
  178. luckGuysMap.set(luckGuy, 1);
  179. }
  180. }
  181.  
  182. luckGuysMap.forEach((v, k, map) => {
  183. luckGuys.push([k, v]);
  184. });
  185. luckGuys.sort((a, b) => (b[1] - a[1]));
  186. return luckGuys;
  187. }
  188.  
  189. function getCandidate(luckGuys, candidateNumber) {
  190. if (candidateNumber > luckGuys.length) {
  191. throw Error('抽奖参与者太少!')
  192. }
  193.  
  194. let output = '本次抽奖备选中奖者:';
  195. for (let i = 0; i < candidateNumber; i++) {
  196. output = `${output}\nNo.${i + 1}:@${luckGuys[i][0]} (幸运指数:${luckGuys[i][1]})`;
  197. }
  198. logout(output);
  199. return output;
  200. }
  201.  
  202. function getRandomIndex(arr) {
  203. return Math.floor(arr.length * Math.random());
  204. }
  205.  
  206. async function request(url) {
  207. logout(`正在请求:${url}`);
  208. const resp = await fetch(url, {
  209. headers: {
  210. Authorization: `Bearer ${token}`,
  211. },
  212. method: 'GET',
  213. });
  214. const date = new Date(resp.headers.get('date'));
  215. const request_id = resp.headers.get('x-request-id');
  216. const runtime = resp.headers.get('x-runtime');
  217. const ratelimit_remaining = resp.headers.get('x-ratelimit-remaining');
  218. logout(`请求 ${url} 完成\n请求时间:${date.toISOString()},API剩余限额:${ratelimit_remaining},x-runtime${runtime},x-request-id${request_id}`);
  219. return await resp.json();
  220. }
  221.  
  222. function logout(text) {
  223. console.log(text);
  224. if (lotterLog) {
  225. lotterLog = lotterLog + '\n' + text;
  226. } else {
  227. lotterLog = text;
  228. }
  229. }
  230.  
  231. function saveFile(data, filename, type) {
  232. const file = new Blob([data], { type: type });
  233. const a = document.createElement('a');
  234. const url = URL.createObjectURL(file);
  235. a.href = url;
  236. a.download = filename;
  237. document.body.appendChild(a);
  238. a.click();
  239. setTimeout(function () {
  240. document.body.removeChild(a);
  241. window.URL.revokeObjectURL(url);
  242. }, 0);
  243. }
  244.  
  245. async function postStatus(text, in_reply_to_id, visibility = 'public') {
  246. const postDate = {
  247. 'in_reply_to_id': in_reply_to_id,
  248. 'media_ids': [],
  249. 'poll': null,
  250. 'sensitive': false,
  251. 'spoiler_text': '',
  252. 'status': text,
  253. 'visibility': visibility,
  254. };
  255.  
  256. logout(`发送嘟文中……\n嘟文内容:\n${text}\n回复嘟文ID${in_reply_to_id}\n可见范围:${visibility}`);
  257. const resp = await fetch(API.status, {
  258. 'headers': {
  259. 'Content-Type': 'application/json;charset=utf-8',
  260. 'Authorization': `Bearer ${token}`,
  261. },
  262. 'body': JSON.stringify(postDate),
  263. 'method': 'POST',
  264. 'mode': 'cors',
  265. });
  266. const date = new Date(resp.headers.get('date'));
  267. const request_id = resp.headers.get('x-request-id');
  268. const runtime = resp.headers.get('x-runtime');
  269. const ratelimit_remaining = resp.headers.get('x-ratelimit-remaining');
  270. logout(`嘟文发送完成,完成请求时间:${date.toISOString()},API剩余限额:${ratelimit_remaining},x-runtime${runtime},x-request-id${request_id}`);
  271. return await resp.json();
  272. }
  273. }

QingJ © 2025

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