您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Автоматизирует внесение вкладов карт в клубах на AnimeStars. Отправляет уведомления в Telegram-чат о текущей карте и её владельцах. Добавляет кнопку добавления недостающих карт в список желаний на странице Колод карт.
// ==UserScript== // @name AnimeStars Club Booster // @name:en AnimeStars Club Booster // @name:ru AnimeStars Club Booster // @namespace http://tampermonkey.net/ // @version 2025-08-12 // @description Автоматизирует внесение вкладов карт в клубах на AnimeStars. Отправляет уведомления в Telegram-чат о текущей карте и её владельцах. Добавляет кнопку добавления недостающих карт в список желаний на странице Колод карт. // @description:ru Автоматизирует внесение вкладов карт в клубах на AnimeStars. Отправляет уведомления в Telegram-чат о текущей карте и её владельцах. Добавляет кнопку добавления недостающих карт в список желаний на странице Колод карт. // @description:en Automates card contributions in AnimeStars clubs. Sends Telegram chat notifications about the current card and its owners. Adds a button to add missing cards to the wishlist on the Card Decks page. // @author Anton Zelinsky https://t.me/anzeky // @match https://animestars.org/clubs/boost/?id=* // @match https://asstars.tv/clubs/boost/?id=* // @match https://*.asstars.tv/clubs/boost/?id=* // @match https://astars.club/clubs/boost/?id=* // @match https://*.astars.club/clubs/boost/?id=* // @match https://animestars.org/user/*/cards_progress/* // @match https://asstars.tv/user/*/cards_progress/* // @match https://*.asstars.tv/user/*/cards_progress/* // @match https://astars.club/user/*/cards_progress/* // @match https://*.astars.club/user/*/cards_progress/* // @run-at document-idle // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=animestars.org // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @homepageURL https://github.com/AntonZelinsky/AnimeStars_Club_Booster // ==/UserScript== /* * === КАК РАБОТАЕТ АВТОМАТИЧЕСКИЙ ВЗНОС КАРТ === * * Для корректной работы автовзносов: * - Откройте страницу Внесения вкладов (ссылка в клубе) * - Можно открыть страницу заранее — в нужное время скрипт сам начнёт работу * - Не закрывайте вкладку до завершения вкладов (можно свернуть или переключить — это не мешает) * - В 21:01 по Минску (UTC+3) скрипт начнёт вносить карты автоматически * - Раз в 5 минут страница автоматически перезагружается для обновления статистики вкладов * - ⚠️ Важно: должна быть открыта только одна вкладка страницы Внесения вкладов, * иначе возможна блокировка из-за слишком частых запросов */ // === НАСТРОЙКИ АВТОВЗНОСОВ === // Задержка перед нажатием кнопки "Обновить карту" (в секундах) // Используется для управления частотой обновлений текущей карты во вкладке клуба const DELAY_RREFRESH_SEC = 1.4; // Задержка после обновления карты перед внесением (в секундах) // Нужна, чтобы DOM успел полностью обновиться перед кликом "Внести карту" const DELAY_BOOST_AFTER_REFRESH_SEC = 0.2; /* * === ДОПОЛНИТЕЛЬНАЯ НАСТРОЙКА УВЕДОМЛЕНИЙ В TELEGRAM === * * ⚠️ Этот функционал НЕ обязателен. * Если вы ничего не заполняете, будет работать только автоматический функционал внесения вкладов. * * ⚠️ Важно: скрипт, настроенный на отправку уведомлений в Telegram, должен быть активен только у одного пользователя. * Иначе в чат будут поступать дублирующие уведомления от разных людей. * ⚠️ Рекомендуется запускать скрипт с включёнными уведомлениями только администраторам или модераторам клуба. * У обычных участников может проявляться баг сайта: при смене карты список владельцев не всегда обновляется корректно, * из-за чего Telegram-уведомления могут не отправляться или содержать неполные данные. * * В меню Tampermonkey под названием скрипта появляется кнопка «Включить/Выключить уведомления в Telegram» * (доступна только если настроена отправка уведомлений). * Её нужно нажимать каждый день перед началом взносов — иначе уведомления не отправятся. * Это предотвращает дубли, когда уведомления могут включить несколько администраторов. * Включает только один человек в день, остальные оставляют выключено. * * Если не включать — автовзносы работают без уведомлений в Telegram. * * Чтобы Telegram-бот начал отправлять уведомления в ваш чат: * * 1. Заполните переменную `usernameMappingRaw` соответствиями: * username_на_сайте:@telegram_username * (одна пара на строку, без пробелов вокруг двоеточия) * * Не все пользователи имеют публичный Telegram username. * В таких случаях можно использовать их Telegram ID через формат ссылки: * username_на_сайте:<a href="tg://user?id=TelegramID">Имя</a> * * Примеры: * const usernameMappingRaw = ` * AnimeStarsNews:@AnimeStarsNews * admin:<a href="tg://user?id=123123123">AnimeStars Admin</a> * ` * * 2. Установите значение переменной `RAW_TELEGRAM_CHAT_ID`. * - Это ID вашей группы или канала, **в который будут отправляться уведомления со списком владельцев карты**. * - Как получить chat_id: https://pikabu.ru/story/_11099278 * - Пример: * const RAW_TELEGRAM_CHAT_ID = '243547803'; * или * const RAW_TELEGRAM_CHAT_ID = '-100243547803'; * * 3. Добавьте бота @AnimeStarsClubBoosterBot в чат или канал, куда должны приходить уведомления. * - Убедитесь, что бот имеет право отправлять сообщения (в Telegram-канале для этого нужно назначить его администратором с разрешением "Публиковать сообщения"). * - При желании можно создать собственного бота через https://t.me/BotFather. * Полученный токен необходимо записать в переменную `TELEGRAM_BOT_TOKEN`. * После этого вместо @AnimeStarsClubBoosterBot в чат нужно добавить вашего собственного бота. * * После этого скрипт сможет автоматически отправлять сообщения в Telegram * со списком владельцев нужной карты. */ // === НАСТРОЙКИ УВЕДОМЛЕНИЙ В TELEGRAM === // 1. Соответствие username на сайте : Telegram username / ID const RAW_USERNAME_MAPPING = ` `.trim(); // 2. ID чата или канала, куда отправляются уведомления const RAW_TELEGRAM_CHAT_ID = ''; // 3. Токен Telegram-бота, через которого будут отправляться уведомления // По умолчанию используется https://t.me/AnimeStarsClubBoosterBot const TELEGRAM_BOT_TOKEN = '8144505785:AAEgVSP_HFcjWm8VxZOYHXLI7dy6XMpqGmw'; // Задержка перед отправкой уведомления в Telegram (в секундах) const DELAY_SEND_MESSAGE__SEC = 4; const USERNAME_MAPPING = (() => { const entries = RAW_USERNAME_MAPPING .split('\n') .map(line => { const match = line.trim().match(/^([^:]+):(.*)$/); return match ? [match[1].trim(), match[2].trim()] : null; }) .filter(Boolean); return entries.length > 0 ? Object.fromEntries(entries) : null; })(); const TELEGRAM_CHAT_ID = RAW_TELEGRAM_CHAT_ID.startsWith('-100') // Id чата Telegram должен начинаться с -100 ? RAW_TELEGRAM_CHAT_ID : `-100${RAW_TELEGRAM_CHAT_ID}`; (function () { "use strict" const MAX_LIMIT_CARDS = 600; const COOKIE_KEY_CURRENT_BOOST_CARD_ID = 'CURRENT_BOOST_CARD_ID'; const COOKIE_KEY_TG_NOTIF_DATE = 'TG_NOTIFICATIONS_DATE'; const TELEGRAM_API_URL = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}`; let observerInstance = null; let menuCommandId = null; /** * Возвращает текущую дату и время по Минску * @returns {Date} — объект времени по Минску */ function getMinskTime() { const minskTimeString = new Date().toLocaleString("en-US", { timeZone: "Europe/Minsk", hour12: false, }); return new Date(minskTimeString); } /** * Возвращает текущую дату по Минску в формате YYYY-MM-DD * @returns {string} */ function getMinskDateString() { return getMinskTime().toISOString().slice(0, 10); } /** * Возвращает объект времени 21:01 по Минску для переданной даты * @param {Date} nowMinskTime — текущее время по Минску * @returns {Date} — объект времени 21:01 по Минску */ function getTarget2101MinskTime(nowMinskTime) { const targetTime = new Date(nowMinskTime); targetTime.setHours(21, 1, 0, 2); return targetTime; } /** * Возвращает количество секунд до 21:01 по Минску * @returns {number} — количество секунд до 21:01 */ function getUntil2101MinskSeconds() { const nowMinskTime = getMinskTime(); const targetTime = getTarget2101MinskTime(nowMinskTime); const diffMs = targetTime - nowMinskTime; return diffMs > 0 ? Math.floor(diffMs / 1000) : 0; } /** * Асинхронная задержка на заданное количество секунд * @param {number} seconds — количество секунд * @returns {Promise<boolean>} — промис, который резолвится через заданное время */ function sleep(seconds) { return new Promise(resolve => setTimeout(() => resolve(true), seconds * 1000)); } /** * Перезагружает страницу через 5 минут (для обновления статистики) */ function reloadPageAfter5min() { DLEPush.info('Страница перезагрузится через 5 минут.') setTimeout(() => { location.reload(); }, 5 * 60 * 1000); } /** * Проверяет, достигнут ли лимит внесённых карт * @returns {boolean} — true, если лимит достигнут */ function isBoostLimitReached() { const limitCounter = document.querySelector('.boost-limit').innerText; if (MAX_LIMIT_CARDS == limitCounter) { console.info(`💳 Лимит карт исчерпан: ${new Date().toLocaleTimeString()}.`); DLEPush.info(`💳 Лимит карт исчерпан: ${new Date().toLocaleTimeString()}.`); return true; } return false; } /** * Преобразует количество секунд в строку вида "X ч Y мин Z сек" * @param {number} seconds — количество секунд * @returns {string} — строка с форматированным временем */ function formatTimeLeft(seconds) { const hrs = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; const parts = []; if (hrs > 0) parts.push(`${hrs} ч`); if (mins > 0) parts.push(`${mins} мин`); if (secs > 0 || parts.length === 0) parts.push(`${secs} сек`); return parts.join(' '); } /** * Получает строку из localStorage * @param {string} key — Ключ * @param {string} [defaultValue=null] — Значение по умолчанию, если ключ не найден * @returns {string|null} — Строка из хранилища или defaultValue */ function getStorageValue(key, defaultValue = null) { const value = localStorage.getItem(key); return value === null ? defaultValue : value; } /** * Записывает строку в localStorage * @param {string} key — Ключ * @param {string} value — Строка */ function upsertStorageValue(key, value) { localStorage.setItem(key, value); } /** * Получает URL изображения текущей карты * @returns {string} — абсолютный URL изображения карты */ function getCardImageUrl() { const imgElement = document.querySelector('.club-boost__image.anime-cards__item img'); return new URL(imgElement.getAttribute('src'), location.origin).href; } /** * Отправляет уведомление в канал Telegram со списком пользователях, у которых есть нужная карта для взноса. * Сообщение отправляется только для новой карты. */ function sendMessageToTelegramAboutDutyUsernames() { if (!isTelegramNotificationConfigured()) return; const refreshBtn = document.querySelector('.button.button--primary.club__boost__refresh-btn'); const currentBoostCardId = refreshBtn ? refreshBtn.dataset.cardId : null; const lastBoostCardId = getStorageValue(COOKIE_KEY_CURRENT_BOOST_CARD_ID); if (!refreshBtn || lastBoostCardId === currentBoostCardId) return; const users = Array.from(document.querySelectorAll('.club-boost__user')) .map(user => { // Извлекаем UserName из ссылки вида "/user/UserName/" const link = user.querySelector('a[href^="/user/"]'); const href = link.getAttribute('href'); return href.slice(6, -1); }) .filter(Boolean); if (users.length === 0) return; const usernames = users.map(name => USERNAME_MAPPING[name] || name); const result = `Карта <code>${currentBoostCardId}</code>: ${usernames.join(', ')}`; console.log(`Отправлено в телеграм: ${result}`); DLEPush.info(result, 'Отправлено в телеграм:'); const imageUrl = getCardImageUrl(); sendTelegramMessage(result, imageUrl); upsertStorageValue(COOKIE_KEY_CURRENT_BOOST_CARD_ID, currentBoostCardId); } /** * Отправляет сообщение и/или фото в Telegram * @param {string} text — текст сообщения * @param {string|null} imageUrl — (опц.) URL картинки */ function sendTelegramMessage(text, imageUrl = null) { const endpoint = imageUrl ? 'sendPhoto' : 'sendMessage'; const url = `${TELEGRAM_API_URL}/${endpoint}`; const payload = imageUrl ? { chat_id: TELEGRAM_CHAT_ID, photo: imageUrl, caption: text, parse_mode: 'HTML' } : { chat_id: TELEGRAM_CHAT_ID, text: text, parse_mode: 'HTML' }; fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(res => res.json()) .then(data => { if (!data.ok) console.error('Telegram error:', data); }) .catch(err => console.error('Fetch error:', err)); } /** * Проверяет, были ли включены уведомления в Telegram сегодня * @returns {boolean} */ function areTelegramNotificationsEnabledToday() { const savedDate = getStorageValue(COOKIE_KEY_TG_NOTIF_DATE); return savedDate === getMinskDateString(); } /** * Проверяет, настроены ли уведомления в Telegram. * Возвращает true, если переменные USERNAME_MAPPING, TELEGRAM_BOT_TOKEN и TELEGRAM_CHAT_ID корректно заполнены. * @returns {boolean} */ function isTelegramNotificationConfigured() { return USERNAME_MAPPING !== null && TELEGRAM_BOT_TOKEN !== '' && TELEGRAM_CHAT_ID !== ''; } /** * Переключает состояние Telegram-уведомлений */ function toggleTelegramNotifications() { if (areTelegramNotificationsEnabledToday()) { // Выключить уведомления в Telegram upsertStorageValue(COOKIE_KEY_TG_NOTIF_DATE, null); stopBoostObserver(); DLEPush.info('Telegram-уведомления выключены.'); replaceCommand('Включить уведомления в Telegram'); } else { // Включить уведомления в Telegram DLEPush.info('Telegram-уведомления включены на сегодня.'); replaceCommand('Выключить уведомления в Telegram'); upsertStorageValue(COOKIE_KEY_TG_NOTIF_DATE, getMinskDateString()); observeBoostOwners(); } } /** * Удаление старой команды и регистрация новой для включения и выключения */ function replaceCommand(title) { GM_unregisterMenuCommand(menuCommandId); menuCommandId = GM_registerMenuCommand(title, toggleTelegramNotifications); } /** * Запускает наблюдатель за изменением карты * Следит за изменением карты и отправляет с задержкой уведомление в Telegram при её смене */ function observeBoostOwners() { if (observerInstance) return; const target = document.querySelector('.club-boost--content'); if (!target) return; // Отправка уведомления о текущей карте при первом запуске sendMessageToTelegramAboutDutyUsernames(); let boostChangeTimeoutId = null; // вынеси observerInstance = new MutationObserver(() => { // Очистка таймера для отмены отправки уведомления с предыдущей картой clearTimeout(boostChangeTimeoutId); const contributeBtn = document.querySelector('.button.button--primary.club__boost-btn') if (contributeBtn) { contributeBtn.click(); console.info(`💳 Внесена карта: ${contributeBtn.dataset.cardId}. ${new Date().toLocaleTimeString()}.`); return; } boostChangeTimeoutId = setTimeout(() => { sendMessageToTelegramAboutDutyUsernames(); }, DELAY_SEND_MESSAGE__SEC * 1000); // Установка задержки перед отправкой сообщения в Telegram }); observerInstance.observe(target, { childList: true, subtree: false, }); } /** * Останавливает наблюдатель */ function stopBoostObserver() { if (observerInstance) { observerInstance.disconnect(); observerInstance = null; } } /** * Исправляет внешний CSS код */ function fixStyle() { // Делает ссылки в уведомлениях чёрными, чтобы не сливались с фоном const style = document.createElement('style'); style.textContent = `.DLEPush-notification a { color: #333 !important; }`; document.head.appendChild(style); } /** * Исправляет внешний JS код */ function fixJs() { // Уменьшает задержку отображения предупреждений до 1 секунды DLEPush.warning = function (message, title, life) { return $.jGrowl(message, { header: title ? title : '', theme: 'push-warning', icon: ` <svg width="28" height="28" fill="currentColor" viewBox="0 0 28 28"> <path d="M16 21.484v-2.969c0-0.281-0.219-0.516-0.5-0.516h-3c-0.281 0-0.5 0.234-0.5 0.516v2.969c0 0.281 0.219 0.516 0.5 0.516h3c0.281 0 0.5-0.234 0.5-0.516zM15.969 15.641l0.281-7.172c0-0.094-0.047-0.219-0.156-0.297-0.094-0.078-0.234-0.172-0.375- 0.172h-3.437c-0.141 0-0.281 0.094-0.375 0.172-0.109 0.078-0.156 0.234-0.156 0.328l0.266 7.141c0 0.203 0.234 0.359 0.531 0.359h2.891c0.281 0 0.516-0.156 0.531-0.359zM15.75 1.047l12 22c0.344 0.609 0.328 1.359-0.031 1.969s-1.016 0.984-1.719 0.984h-24c-0.703 0-1.359-0.375-1.719-0.984s-0.375-1.359-0.031- 1.969l12-22c0.344-0.641 1.016-1.047 1.75-1.047s1.406 0.406 1.75 1.047z"> </path> </svg>`.trim(), life: life ? life : 1000 }); }; } /** * Основной цикл внесения вкладов (автоматизация) * @returns {Promise<void>} */ async function handleBoost() { console.log('Внесение вкладов начато.'); console.log(`Последняя карта: ${getStorageValue(COOKIE_KEY_CURRENT_BOOST_CARD_ID)}`); do { const refreshBtn = document.querySelector('.button.button--primary.club__boost__refresh-btn') if (refreshBtn) { refreshBtn.click(); console.log(`🌀 Обновлена карта: ${refreshBtn.dataset.cardId}.`); await sleep(DELAY_BOOST_AFTER_REFRESH_SEC); } const contributeBtn = document.querySelector('.button.button--primary.club__boost-btn'); if (contributeBtn) { contributeBtn.click(); console.info(`💳 Внесена карта: ${contributeBtn.dataset.cardId}. ${new Date().toLocaleTimeString()}.`); await sleep(DELAY_RREFRESH_SEC); } if (isBoostLimitReached()) { break; } } while(await sleep(DELAY_RREFRESH_SEC)) } /** * Запуск автоматизации вкладов, ожидание времени, контроль лимита * @returns {Promise<void>} */ async function runBoost() { console.log(`Начало работы автовкладов. ${new Date().toLocaleTimeString()}.`); reloadPageAfter5min() if (isTelegramNotificationConfigured()) { // Регистрируем команды в меню TemperMonkey if (areTelegramNotificationsEnabledToday()) { replaceCommand('Выключить уведомления в Telegram'); } else { // Включить уведомления в Telegram replaceCommand('Включить уведомления в Telegram'); } } const secondsLeft = getUntil2101MinskSeconds(); if (isBoostLimitReached() && secondsLeft > 0) { console.log(`До 21:01 по Мінску осталось ${formatTimeLeft(secondsLeft)}.`); await sleep(secondsLeft); location.reload(); return; } if (isTelegramNotificationConfigured()) { DLEPush.info(`🔢 Число участников для уведомления в чате Telegram: ${Object.keys(USERNAME_MAPPING).length}.`); } fixStyle(); fixJs(); if (isTelegramNotificationConfigured() && areTelegramNotificationsEnabledToday()) { observeBoostOwners(); } await handleBoost(); console.log('🏁 Внесение вкладов завершено.'); } if (/\/clubs\/boost\//.test(window.location.pathname)) { runBoost(); } /** * Добавляет кнопку "Добавить недостающие карты в список желаний" на странице с колодами карт */ if (/\/user\/[^\/]+\/cards_progress\//.test(window.location.pathname)) { function injectCardsProgressButtons() { const userAnimeDivs = document.querySelectorAll('div.user-anime'); userAnimeDivs.forEach(div => { const progressDiv = div.querySelector('div.user-anime__progress'); const button = div.querySelector('button.update-my-progress'); // Извлекаем ID из строки вида `UpdateMyProgress('123456')` const animeId = button?.getAttribute('onclick')?.match(/UpdateMyProgress\('(\d+)'\)/)?.[1] || '000000'; progressDiv?.insertAdjacentHTML('afterend', ` <div class="cards-progress card-anime-list__add-btn" data-anime="${animeId}" style="display:block"> <i class="ass-cards"></i> Добавить недостающие в список желаний </div> `); }); } injectCardsProgressButtons(); } })()