- // ==UserScript==
- // @name 长毛象抽奖脚本
- // @namespace https://blog.bgme.me
- // @match https://bgme.me/*
- // @match https://bgme.bid/*
- // @match https://c.bgme.bid/*
- // @grant none
- // @run-at document-end
- // @version 1.0.0
- // @author bgme
- // @description 点击“开始抽奖”后,随机抽出五名中奖候选者。
- // @supportURL https://github.com/yingziwu/Greasemonkey/issues
- // @license AGPL-3.0-or-later
- // ==/UserScript==
-
- window.addEventListener('load', function () {
- activateMastodonLottery();
- }, false)
-
- function chromeClickChecker(event) {
- return (
- event.target.tagName.toLowerCase() === 'i' &&
- event.target.classList.contains('fa-ellipsis-h') &&
- document.querySelector('div.dropdown-menu') === null
- );
- }
-
- function firefoxClickChecker(event) {
- return (
- event.target.tagName.toLowerCase() === 'button' &&
- event.target.classList.contains('icon-button') &&
- document.querySelector('div.dropdown-menu') === null
- );
- }
-
- function activateMastodonLottery() {
- document.querySelector('body').addEventListener('click', function (event) {
- if (chromeClickChecker(event) || firefoxClickChecker(event)) {
- // Get the status for this event
- let status = event.target.parentNode.parentNode.parentNode.parentNode.parentNode;
- if (status.className.match('detailed-status__wrapper')) {
- addLotteryLink(status);
- }
- };
- }, false);
- }
-
- function addLotteryLink(status) {
- setTimeout(function () {
- const lotteryStatusUrl = status.querySelector('.detailed-status__datetime').getAttribute('href');
- const dropdown = document.querySelector('div.dropdown-menu ul');
- const separator = dropdown.querySelector('li.dropdown-menu__separator');
-
- const listItem = document.createElement('li');
- listItem.classList.add('dropdown-menu__item');
- listItem.classList.add('mastodon__lottery');
-
- const link = document.createElement('a');
- link.setAttribute('href', '#');
- link.setAttribute('target', '_blank');
- link.textContent = '开始抽奖';
-
- link.addEventListener('click', function (e) {
- e.preventDefault();
- if (!window.lotteryRunning) {
- window.lotteryRunning = true;
- link.textContent = '抽奖中,请等待……';
- run(lotteryStatusUrl).then(() => { window.lotteryRunning = false }).catch(() => { window.lotteryRunning = false });
- }
- }, false);
-
- listItem.appendChild(link);
- dropdown.insertBefore(listItem, separator);
- }, 100);
- }
-
- async function run(lotteryStatusUrl, lotteryType = 'reblog', candidateNumber = 5) {
- // lotteryStatusUrl 抽奖嘟文URL
- // lotteryType 抽奖类型:转发(reblog),收藏(favourite)
- // candidateNumber 候选中奖者人数
-
- const domain = document.location.hostname;
- const token = JSON.parse(document.querySelector('#initial-state').text).meta.access_token;
- const API = {
- 'verify': `https://${domain}/api/v1/accounts/verify_credentials`,
- 'notifications': `https://${domain}/api/v1/notifications`,
- 'status': `https://${domain}/api/v1/statuses/`,
- };
- const searchParamMap = new Map([
- ['reblog', 'exclude_types[]=follow&exclude_types[]=follow_request&exclude_types[]=favourite&exclude_types[]=mention&exclude_types[]=poll'],
- ['favourite', 'exclude_types[]=follow&exclude_types[]=follow_request&exclude_types[]=reblog&exclude_types[]=mention&exclude_types[]=poll'],
- ]);
- const searchParam = new URLSearchParams(searchParamMap.get(lotteryType));
-
- const statusID = lotteryStatusUrl.match(/(\d+)$/)[0];
- let statusTNumber;
- let lotterLog;
-
-
- logout(`开始抽奖……\n当前浏览器:${navigator.userAgent}\n开始时间:${(new Date()).toISOString()}`);
- logout(`抽奖嘟文:${lotteryStatusUrl},抽奖类型:${lotteryType},候选中奖者人数:${candidateNumber}\n\n`);
- let verify;
- [verify, statusTNumber] = await doVerify(API, lotteryStatusUrl, statusID, lotteryType, statusTNumber);
- if (!verify) {
- throw Error('抽奖嘟文非本人发送');
- }
- const matchAccouts = await getmatchAccouts(API, statusID, statusTNumber, searchParam);
- randomTest(matchAccouts);
- const luckGuys = getLuckGuy(matchAccouts);
- const cadidatesText = getCandidate(luckGuys, candidateNumber);
- const notificationText = `嘿!感谢各位参与本次小抽奖活动。\n${cadidatesText}\n\n希望这条艾特您的信息没有造成骚扰,如您对奖品感兴趣请和我私信联系吧?`;
- await postStatus(notificationText, statusID, 'public');
- logout(`抽奖结束!\n结束时间:${(new Date()).toISOString()}`);
- saveFile(lotterLog, `lotterLog-${Date.now()}.log`, 'text/plain; charset=utf-8');
-
-
- async function doVerify(API, lotteryStatusUrl, statusID, lotteryType, statusTNumber) {
- const v = await request(API.verify);
- const s = await request(`${API.status}${statusID}`);
- logout(`抽奖嘟文URL:${lotteryStatusUrl}\n回复数:${s.replies_count},转发数:${s.reblogs_count},收藏数:${s.favourites_count}`);
-
- const numbers = new Map([['reblog', s.reblogs_count], ['favourite', s.favourites_count]]);
- if (numbers.has(lotteryType)) {
- statusTNumber = numbers.get(lotteryType);
- } else {
- throw Error('抽奖类型设置不正确');
- }
-
- if (v.acct === s.account.acct && (new URL(s.account.url)).hostname === (new URL(lotteryStatusUrl)).hostname) {
- return [true, statusTNumber];
- } else {
- return [false, statusTNumber];
- }
- }
-
- async function getmatchAccouts(API, statusID, statusTNumber, searchParam) {
- const matchAccouts = [];
-
- while (matchAccouts.length !== statusTNumber) {
- const nlist = await request(`${API.notifications}?${searchParam.toString()}`);
- searchParam.set('max_id', nlist.slice(-1)[0].id);
-
- nlist.forEach((obj) => {
- if (obj.status.id === statusID) {
- matchAccouts.push(obj.account.acct);
- }
- });
- }
-
- matchAccouts.sort();
- logout(`共有${matchAccouts.length}名符合条件的抽奖参与者\n她们是:`);
- matchAccouts.forEach(logout);
-
- return matchAccouts;
- }
-
- function randomTest(matchAccouts) {
- logout('随机函数测试:');
- const testResults = [];
- const n = 20;
- for (let i = 0; i < (n * 20); i++) {
- testResults.push(getRandomIndex(matchAccouts));
- }
- for (let i = 0; i < n; i++) {
- logout(testResults.slice((i * 20), ((i + 1) * 20)).join(', '));
- }
- }
-
- function getLuckGuy(matchAccouts) {
- const luckGuys = [];
- const n = matchAccouts.length;
- const luckGuysMap = new Map();
- for (let i = 0; i < (n * 100); i++) {
- const luckGuy = matchAccouts[getRandomIndex(matchAccouts)];
- if (luckGuysMap.get(luckGuy)) {
- luckGuysMap.set(luckGuy, luckGuysMap.get(luckGuy) + 1);
- } else {
- luckGuysMap.set(luckGuy, 1);
- }
- }
-
- luckGuysMap.forEach((v, k, map) => {
- luckGuys.push([k, v]);
- });
- luckGuys.sort((a, b) => (b[1] - a[1]));
- return luckGuys;
- }
-
- function getCandidate(luckGuys, candidateNumber) {
- if (candidateNumber > luckGuys.length) {
- throw Error('抽奖参与者太少!')
- }
-
- let output = '本次抽奖备选中奖者:';
- for (let i = 0; i < candidateNumber; i++) {
- output = `${output}\nNo.${i + 1}:@${luckGuys[i][0]} (幸运指数:${luckGuys[i][1]})`;
- }
- logout(output);
- return output;
- }
-
- function getRandomIndex(arr) {
- return Math.floor(arr.length * Math.random());
- }
-
- async function request(url) {
- logout(`正在请求:${url}`);
- const resp = await fetch(url, {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- method: 'GET',
- });
- const date = new Date(resp.headers.get('date'));
- const request_id = resp.headers.get('x-request-id');
- const runtime = resp.headers.get('x-runtime');
- const ratelimit_remaining = resp.headers.get('x-ratelimit-remaining');
- logout(`请求 ${url} 完成\n请求时间:${date.toISOString()},API剩余限额:${ratelimit_remaining},x-runtime:${runtime},x-request-id:${request_id}`);
- return await resp.json();
- }
-
- function logout(text) {
- console.log(text);
- if (lotterLog) {
- lotterLog = lotterLog + '\n' + text;
- } else {
- lotterLog = text;
- }
- }
-
- function saveFile(data, filename, type) {
- const file = new Blob([data], { type: type });
- const a = document.createElement('a');
- const url = URL.createObjectURL(file);
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- setTimeout(function () {
- document.body.removeChild(a);
- window.URL.revokeObjectURL(url);
- }, 0);
- }
-
- async function postStatus(text, in_reply_to_id, visibility = 'public') {
- const postDate = {
- 'in_reply_to_id': in_reply_to_id,
- 'media_ids': [],
- 'poll': null,
- 'sensitive': false,
- 'spoiler_text': '',
- 'status': text,
- 'visibility': visibility,
- };
-
- logout(`发送嘟文中……\n嘟文内容:\n${text}\n回复嘟文ID:${in_reply_to_id}\n可见范围:${visibility}`);
- const resp = await fetch(API.status, {
- 'headers': {
- 'Content-Type': 'application/json;charset=utf-8',
- 'Authorization': `Bearer ${token}`,
- },
- 'body': JSON.stringify(postDate),
- 'method': 'POST',
- 'mode': 'cors',
- });
- const date = new Date(resp.headers.get('date'));
- const request_id = resp.headers.get('x-request-id');
- const runtime = resp.headers.get('x-runtime');
- const ratelimit_remaining = resp.headers.get('x-ratelimit-remaining');
- logout(`嘟文发送完成,完成请求时间:${date.toISOString()},API剩余限额:${ratelimit_remaining},x-runtime:${runtime},x-request-id:${request_id}`);
- return await resp.json();
- }
- }