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