长毛象抽奖脚本

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
  }
}