Несколько лет назад на блоге уже была подобная статья про ВКонтакте, но время идет, стриминговые платформы в своем расцвете, как и розыгрыши призов среди зрителей. В частности, на платформе Twitch популярен чат-бот Nightbot, через который осуществляется как модерация и реализация полезного функционала для канала (прямо как у ботов в IRC), так и организация розыгрышей призов через панель управления. Рассмотрим подробнее, как подменить победителя с помощью расширения Tampermonkey в браузере Google Chrome (естественно, это актуально только с позиции организатора розыгрыша / стримера).
Для начала, давайте проанализируем, что происходит при нажатии на кнопку "Roll it!" в панели управления Nightbot.
Анализируем HTML-представление страницы (используем контекстное меню при нажатии по элементу и пункт Inspect, а не View Page Source) и наблюдаем следующее:
1 |
<button type="button" class="btn btn-block btn-outline btn-primary ng-click-active" ng-click="giveaway()" tooltip-placement="top" uib-tooltip="Start Giveaway">Roll It!</button> |
Мы видим, что при нажатии вызывается функция 'giveaway', которая где-то определена. Также, если переместиться в начало кода страницы (или воспользоваться вкладкой Sources в Developer Tools), то мы увидим, что в контексте подгружается Javascript-файл с именем вида 'app-f31e8d58.js', например: вот. Файл сжат, поэтому стоит воспользоваться http://beautifier.io/ или встроенным в Developer Tools браузера Google Chrome аналогичным инструментом.
Нас интересует фрагмент кода, который начинается со следующей строки (примерно 2040 строка в распакованном представлении кода):
1 |
angular.module("nightbot").controller("giveaways", ["$scope", "$timeout", "$modal", "$q", "API", "UserData", "Alert", "WS", function(e, t, n, s, o, a, i, l) { |
Также обратите внимание на следующие фрагменты кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function r(e) { for (var t, n, s = e.length; 0 !== s; ) { n = Math.floor(Math.random() * s), s -= 1, t = e[s], e[s] = e[n], e[n] = t; } return e } e.users = {}, e.computedUsers = [], e.eligibleUsers = 0, e.winner = null; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
e.giveaway = function() { if (!u().length) return void i.new("error", "There are no eligible users to win the giveaway."); var n; switch (e.giveawayType) { case "number": n = g; break; case "keyword": n = v; break; default: n = b } n(function(n) { e.winner = null, t(function() { o.post("/channel/send", { data: { message: n.displayName + " has won the giveaway." } }), o.get("/channel/lookup_user", { params: { name: n.name } }).success(function(e) { n.following = e.user.following, n.avatar = e.user.avatar, n.profileUrl = e.user.profileUrl, n.messageUrl = e.user.messageUrl }), e.giveawayOptions.winnersIneligible && (c.push(n.providerId), n.eligible = !1), e.winner = n, h() }) }) } ; var g = function(t) { e.randomNumberWinner = null, e.randomNumber = Math.floor(Math.random() * (e.giveawayOptions.randomNumberMaximum - e.giveawayOptions.randomNumberMinimum) + e.giveawayOptions.randomNumberMinimum), o.post("/channel/send", { data: { message: "The giveaway has started. To win, enter a number between " + e.giveawayOptions.randomNumberMinimum + " and " + e.giveawayOptions.randomNumberMaximum + "." } }); var n = e.$watch("randomNumberWinner", function(e, s) { return -1 === e ? n() : void (e !== s && (t(e), n())) }) } , b = function(t) { var n = u() , s = []; n.forEach(function(t) { var n = 1; ("subscriber" === t.userType || t.subscriber) && e.luck.subscribers > 1 && (n = e.luck.subscribers), ("regulars" === t.userType || t.regular) && e.luck.regulars > 1 && (n = e.luck.regulars); for (var o = 0; n > o; o++) s.push(t) }), r(s), t(s[0]) } |
И сразу отметим нюансы:
1. Из-за использования AngularJS перехват внутренних функций представляет определенную сложность.
2. Прямая модификация массива e.users
лишена смысла, т.к. победитель выбирается случайным образом из него, а урезание массива до 1 элемента приводит к дополнительным проблемам при отображении списка участников и их общего числа в панели управления Nightbot.
3. Прямая модификация e.winner
приводит к визуальным гличам и необходимости менять ряд элементов DOM вручную.
В итоге остановимся на модификации результата функции перемешивания массива участников (e.users
), путем перехвата вызова функции Math.floor. Фактически нас интересует следующий фрагмент кода:
1 2 3 4 5 6 7 8 9 10 11 |
function r(e) { for (var t, n, s = e.length; 0 !== s; ) { n = Math.floor(Math.random() * s), s -= 1, t = e[s], e[s] = e[n], e[n] = t; } return e } |
В данном цикле происходит перемешивание элементов, но мы можем это предотвратить, если Math.floor
будет возвращать 0 на каждой итерации, кроме последней. На последней итерации мы вернем значение, которое позволит нам переместить желаемого участника в начало массива. Именно там после перемешивания находится победитель. Обратите внимание на самый конец фрагмента кода, упомянутого ранее (функция 't' отвечает за отображение победителя):
1 2 |
r(s), t(s[0]) |
В итоге получился следующий скрипт для Tampermonkey (обратите внимание на комментарии в коде):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
// ==UserScript== // @name Nightbot giveaways // @version 0.1 // @match https://nightbot.tv/* // @description Predictable winner generation for Nightbot giveaways (https://docs.nightbot.tv/control-panel/giveaways) // @author Kaimi // @homepageURL https://kaimi.io/ // @namespace https://greasyfork.org/users/228137 // ==/UserScript== // Ник победителя var winner = 'lirik'; // Задержка после загрузки страницы (в мс) var loadDelay = 3000; // Режим отладки var debugMode = false; // Глобальные переменные var origMathFloor = Math.floor; var throwCtr = 0; var usersCtr = 0; var dryRun = 1; // Прагма run-at для Tampermonkey не работает из-за динамической генерации UI на странице var observer = new MutationObserver(resetTimer); // Таймер, срабатывающий через заданное число миллисекунд после того, как страница загрузилась var timer = setTimeout(action, loadDelay, observer); observer.observe(document, {childList: true, subtree: true}); function resetTimer(changes, observer) { clearTimeout(timer); timer = setTimeout(action, loadDelay, observer); } // Обработчик таймера function action(o) { o.disconnect(); setHandler(); } // Устанавливаем перехват для функции Math.floor и для события onclick для двух кнопок запуска розыгрыша function setHandler() { Math.floor = customMathFloor; var totalButtons = angular.element("button[ng-click='giveaway()']").length; for(var i = 0; i < totalButtons; i++) { angular.element("button[ng-click='giveaway()']")[i].onclick = function() { throwCtr = 0; usersCtr = 0; dryRun = 1; }; } } // Предсказуемое 'перемешивание' массива участников function customMathFloor(n) { if (debugMode) console.log("Call -> " + n + "; throwCtr -> " + throwCtr + "; usersCtr -> " + usersCtr + "; dryRun -> " + dryRun); if (dryRun) { usersCtr = getUsersTotal(); dryRun = 0; } if (usersCtr - 1 == throwCtr && usersCtr > 1) { throwCtr++; var winnerPos = getWinnerPosition(); if (winnerPos != -1) { if (debugMode) { console.log("Target throw number: " + throwCtr); console.log("Winner position: " + winnerPos); } // На последней итерации возвращаем значение, которое приводит к перемещению желаемого участника в начало списка return winnerPos == 0 ? usersCtr - 1 : winnerPos - 1; } } throwCtr++; return 0; } // Определение позиции желаемого победителя (по нику) в списке участников function getWinnerPosition() { var users = getUsers(); if (users) { var ctr = 0; for (var user in users) { if (users[user].name == winner) return ctr; ctr++; } } console.log("Can't find '" + winner + "' in 'users' array"); return -1; } // Получение списка участников с доп. обработкой // Она необходима, чтобы добиться идентичности порядка при перечислении ключей для ассоциативного массива в JS // Порядок извлечения ключей (в соответствии со спецификацией ES2015) должен быть постоянен для нечисловых значений // Однако применение фильтра (взят из оригинального кода) его нарушает function getUsers() { var users = angular.element("button[ng-click='giveaway()']").scope().users; if (users) { return Object.values(users).filter(function(e) { return e.eligible }); } else { console.log("Can't find 'users' array"); } return users; } // Общее число участников function getUsersTotal() { return Object.keys(angular.element("button[ng-click='giveaway()']").scope().users).length; } |
Скрипт работает в Google Chrome с Tampermonkey, однако, работа в Mozilla Firefox с Greasemonkey не проверялась.
В архиве скрипт с англоязычными комментариями: скачать.
Также актуальная версия доступна на Greasyfork (если будет обновление, то именно там): перейти.
Привет KAIMI вы не смогли бы сделать скрипт на рандомайзер Лизы он Эйр под инстаграм конкурсы заранее спасибо у меня не получается .(((
Прошу прощение за нескромный вопрос и не по теме а будет новый гайд на сзлом easyQuizzy 2,0 а то предыдущий не работает с 2 шага очень щипитильная тема
Модифицированный редактор теста easyQuizzy из статьи нормально открывает версии теста, созданные последней доступной версией программы.
Смысла повторять шаги, кроме как для себя, нет.