Telegram Scraper (Menu Commands v2.3.3 - Human-like Defaults)

Scrapes Telegram, sends to n8n. GUI, auto-start, random channels, more human-like default timings.

  1. // ==UserScript==
  2. // @name Telegram Scraper (Menu Commands v2.3.3 - Human-like Defaults)
  3. // @name:ru Telegram Scraper (Команды меню v2.3.3 - Человекоподобные значения по умолчанию)
  4. // @namespace http://tampermonkey.net/
  5. // @version 2.3.3
  6. // @description Scrapes Telegram, sends to n8n. GUI, auto-start, random channels, more human-like default timings.
  7. // @description:ru Собирает сообщения из Telegram, отправляет в n8n. GUI, автозапуск, случайные каналы, более человекоподобные тайминги по умолчанию.
  8. // @author Igor Lebedev (Adapted by Gemini Pro)
  9. // @license MIT
  10. // @homepageURL https://github.com/LebedevIV/telegram-web-scraper
  11. // @supportURL https://github.com/LebedevIV/telegram-web-scraper/issues
  12. // @match https://web.telegram.org/k/*
  13. // @match https://web.telegram.org/a/*
  14. // @match https://web.telegram.org/z/*
  15. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  16. // @grant GM_xmlhttpRequest
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_getValue
  19. // @grant GM_setValue
  20. // @grant GM_info
  21. // @run-at document-idle
  22. // ==/UserScript==
  23.  
  24. /*
  25. ENGLISH COMMENTS:
  26. This script scrapes messages from Telegram channels and sends them to an n8n webhook.
  27. Key Features:
  28. - Single/Multi-channel scraping.
  29. - GM_config GUI for settings with more human-like default timings.
  30. - Message age limit.
  31. - Channel navigation.
  32. - Randomized delays.
  33. - Tampermonkey menu commands.
  34. - Scheduled auto-start for multi-channel scraping.
  35. - Option to randomize channel scraping order.
  36.  
  37. РУССКИЕ КОММЕНТАРИИ:
  38. Этот скрипт собирает сообщения из Telegram-каналов и отправляет их на веб-хук n8n.
  39. Ключевые особенности:
  40. - Сбор данных с одного/нескольких каналов.
  41. - GUI настроек через GM_config с более человекоподобными таймингами по умолчанию.
  42. - Ограничение по возрасту сообщений.
  43. - Навигация по каналам.
  44. - Рандомизированные задержки.
  45. - Команды управления через меню Tampermonkey.
  46. - Автоматический запуск сбора со всех каналов по расписанию.
  47. - Опция случайного порядка сбора каналов.
  48. */
  49.  
  50. (function() {
  51. 'use strict';
  52.  
  53. // --- GLOBAL SCRIPT VARIABLES (NOT SETTINGS) ---
  54. let isScrapingSingle = false;
  55. let isMultiChannelScrapingActive = false;
  56. let currentChannelIndex = 0;
  57. let currentScrapingChannelInfo = null;
  58. let consecutiveScrollsWithoutNewFound = 0;
  59. let autoStartCheckInterval = null;
  60. const LAST_AUTO_SCRAPE_DATE_KEY = 'TeleScraper_lastAutoScrapeDate';
  61.  
  62. // --- SCRIPT CONSTANTS ---
  63. const TARGET_CHANNELS_DATA_ORIGINAL = [
  64. { name: '@e1_news', id: '-1049795479' }, { name: '@RU66RU', id: '-1278627542' },
  65. { name: '@ekb4tv', id: '-1184077858' }, { name: '@rentv_news', id: '-1310155678' },
  66. { name: '@TauNewsEkb', id: '-1424016223' }, { name: '@BEZUMEKB', id: '-1739473739' },
  67. { name: '@zhest_dtp66', id: '-2454557093' }, { name: '@sverdlovskaya_oblasti', id: '-1673288653' },
  68. { name: '@novosti_ekb66', id: '-1662411694' }
  69. ];
  70. let currentTargetChannels = [...TARGET_CHANNELS_DATA_ORIGINAL];
  71. const SETTINGS_REQUIRING_RELOAD = ['N8N_WEBHOOK_URL'];
  72.  
  73. function consoleLog(message, isError = false) {
  74. const prefix = "[TeleScraper]";
  75. if (isError) { console.error(`${prefix} ${message}`); }
  76. else { console.log(`${prefix} ${message}`); }
  77. }
  78. function updateStatusForConsole(message, isError = false) {
  79. consoleLog(message, isError);
  80. }
  81. consoleLog(`v${GM_info.script.version} Script execution started.`);
  82.  
  83. const GM_CONFIG_ID = `TeleScraperConfig_v${GM_info.script.version.replace(/\./g, '_')}`; // на время разработки настройки сбрасываются от версии к версии до дефолтных
  84. // const GM_CONFIG_ID = 'TeleScraperUserSettings'; // Любое уникальное статическое имя // При достижении стабильных версий настройки можно будет сохранять
  85.  
  86. let configFields = {
  87. 'N8N_WEBHOOK_URL': {
  88. 'label': 'N8N Webhook URL:',
  89. 'type': 'text',
  90. 'default': 'http://localhost:5678/webhook/telegram-scraped-news',
  91. 'section': ['Main Settings / Основные настройки'],
  92. },
  93. 'MAX_MESSAGE_AGE_HOURS': {
  94. 'label': 'Max message age (hours):',
  95. 'type': 'int',
  96. 'default': 24,
  97. 'min': 1, 'max': 720
  98. },
  99. 'BASE_SCRAPE_INTERVAL_MS': {
  100. 'label': 'Base scrape interval (ms) (scroll up frequency):',
  101. 'type': 'int',
  102. 'default': 45000, // Increased default / Увеличено значение по умолчанию
  103. 'min': 15000, // Recommended minimum / Рекомендуемый минимум
  104. 'title': 'Main pause between processing message chunks. Recommended min: 15000ms. / Основная пауза между обработкой порций сообщений. Рекомендуемый мин: 15000мс.'
  105. },
  106. 'BASE_SCROLL_PAUSE_MS': {
  107. 'label': 'Pause after scroll action (ms):',
  108. 'type': 'int',
  109. 'default': 7000, // Increased default / Увеличено значение по умолчанию
  110. 'min': 3000, // Recommended minimum / Рекомендуемый минимум
  111. 'title': 'Pause after each scroll. Recommended min: 3000ms. / Пауза после каждой прокрутки. Рекомендуемый мин: 3000мс.'
  112. },
  113. 'BASE_SEND_DELAY_MS': {
  114. 'label': 'Delay before sending each message to n8n (ms):',
  115. 'type': 'int',
  116. 'default': 1500, // Increased default / Увеличено значение по умолчанию
  117. 'min': 500, // Recommended minimum / Рекомендуемый минимум
  118. 'title': 'Pause between sending individual messages. Recommended min: 500ms. / Пауза между отправкой отдельных сообщений. Рекомендуемый мин: 500мс.'
  119. },
  120. 'CONSECUTIVE_SCROLLS_LIMIT': {
  121. 'label': 'Empty scrolls limit (stops channel if no new messages found after N scrolls):',
  122. 'type': 'int',
  123. 'default': 5,
  124. 'min': 2 // At least 2 to be sure / Минимум 2 для уверенности
  125. },
  126. // Auto Start Section
  127. 'AUTO_START_ENABLED': {
  128. 'label': 'Enable Automatic Scraping (All Channels) [reloading the page is required / требуется перезагрузка страницы]:',
  129. 'type': 'checkbox', 'default': false,
  130. 'section': ['Automatic Start / Автоматический запуск'],
  131. 'title': 'If checked, the script will attempt to run "Scrape All Listed Channels" daily at the specified time, if the Telegram Web tab is open. / Если отмечено, скрипт попытается запустить "Собрать со всех каналов" ежедневно в указанное время, если вкладка Telegram Web открыта.'
  132. },
  133. 'AUTO_START_TIME': {
  134. 'label': 'Scheduled Start Time (HH:MM, 24-hour local time):',
  135. 'type': 'text', 'default': '10:00', 'size': 5,
  136. 'title': 'Example: 09:30 for 9:30 AM, 22:15 for 10:15 PM'
  137. },
  138. // Fine-tuning Section
  139. 'RANDOMIZE_CHANNEL_ORDER': {
  140. 'label': 'Randomize channel order for multi-scrape:',
  141. 'type': 'checkbox', 'default': true,
  142. 'section': ['Fine-tuning (pauses and attempts) / Тонкие настройки (паузы и попытки)'],
  143. 'title': 'If checked, the order of channels from TARGET_CHANNELS_DATA will be shuffled before each multi-channel scrape. / Если отмечено, порядок каналов из TARGET_CHANNELS_DATA будет перемешан перед каждым многоканальным сбором.'
  144. },
  145. 'NAVIGATION_INITIATION_PAUSE_MS': {
  146. 'label': 'Pause after navigation hash change (ms):',
  147. 'type': 'int', 'default': 4000, 'min': 2000, // Increased default & min / Увеличены значения по умолчанию и минимум
  148. 'title': 'Recommended min: 2000ms. / Рекомендуемый мин: 2000мс.'
  149. },
  150. 'CHANNEL_ACTIVATION_ATTEMPT_PAUSE_MS': {
  151. 'label': 'Pause between channel activation attempts (ms):',
  152. 'type': 'int', 'default': 1000, 'min': 500, // Increased default & min / Увеличены значения по умолчанию и минимум
  153. 'title': 'Recommended min: 500ms. / Рекомендуемый мин: 500мс.'
  154. },
  155. 'MAX_CHANNEL_ACTIVATION_ATTEMPTS': { 'label': 'Max channel activation attempts:', 'type': 'int', 'default': 25, 'min': 1 },
  156. 'BASE_SCROLL_ACTION_PAUSE_MS': {
  157. 'label': 'Short pause before/after scroll action (ms):',
  158. 'type': 'int', 'default': 800, 'min': 200, // Increased default & min / Увеличены значения по умолчанию и минимум
  159. 'title': 'Recommended min: 200ms. / Рекомендуемый мин: 200мс.'
  160. },
  161. 'BASE_SCROLL_BOTTOM_PROG_PAUSE_MS': {
  162. 'label': 'Pause during programmatic scroll to bottom (ms):',
  163. 'type': 'int', 'default': 1200, 'min': 500, // Increased default & min / Увеличены значения по умолчанию и минимум
  164. 'title': 'Recommended min: 500ms. / Рекомендуемый мин: 500мс.'
  165. },
  166. 'BASE_SCROLL_BOTTOM_CLICK_PAUSE_MS': {
  167. 'label': 'Pause after "scroll to bottom" button click (ms):',
  168. 'type': 'int', 'default': 3000, 'min': 1500, // Increased default & min / Увеличены значения по умолчанию и минимум
  169. 'title': 'Recommended min: 1500ms. / Рекомендуемый мин: 1500мс.'
  170. },
  171. 'SCROLL_BOTTOM_PROGRAMMATIC_ITERATIONS': { 'label': 'Programmatic scroll to bottom iterations:', 'type': 'int', 'default': 3, 'min': 1 },
  172. 'MAX_GO_TO_BOTTOM_CLICKS': { 'label': 'Max clicks on "scroll to bottom" button (with badge):', 'type': 'int', 'default': 3, 'min': 0 },
  173. 'RANDOMNESS_FACTOR_MAJOR': {
  174. 'label': 'Randomness factor for major pauses (0.0-1.0):',
  175. 'type': 'float', 'default': 0.4, 'min': 0, 'max': 1, // Increased default / Увеличено значение по умолчанию
  176. 'title': 'Adds variability. 0.3 means +/-15%. Higher values = more random. / Добавляет вариативности. 0.3 означает +/-15%. Большие значения = больше случайности.'
  177. },
  178. 'RANDOMNESS_FACTOR_MINOR': {
  179. 'label': 'Randomness factor for minor pauses (0.0-1.0):',
  180. 'type': 'float', 'default': 0.25, 'min': 0, 'max': 1, // Increased default / Увеличено значение по умолчанию
  181. 'title': 'Adds variability. 0.15 means +/-7.5%. Higher values = more random. / Добавляет вариативности. 0.15 означает +/-7.5%. Большие значения = больше случайности.'
  182. },
  183. 'USE_FOCUS_IN_SCROLL_UP': { 'label': 'Use focus() during scroll up (experimental):', 'type': 'checkbox', 'default': false }
  184. };
  185.  
  186. for (const key in configFields) {
  187. if (configFields.hasOwnProperty(key)) {
  188. let labelSuffix = ` (по умолчанию: ${configFields[key].default})`;
  189. if (SETTINGS_REQUIRING_RELOAD.includes(key)) {
  190. labelSuffix += ' [требуется перезагрузка / reload required]';
  191. }
  192. // Add min recommendation to label if not already in title
  193. // Добавление рекомендации по минимуму в метку, если ее еще нет в title
  194. if (configFields[key].min && !configFields[key].title?.includes('Recommended min') && !configFields[key].title?.includes('Рекомендуемый мин')) {
  195. labelSuffix += ` (рек. мин: ${configFields[key].min})`;
  196. }
  197. configFields[key].label += labelSuffix;
  198. }
  199. }
  200.  
  201. const configEventHandlers = { /* ... (CSS and other handlers - no changes from v2.3.2) ... */
  202. 'open': function(doc) {
  203. const urlFieldInputId = `${GM_CONFIG_ID}_field_N8N_WEBHOOK_URL`;
  204. const style = doc.createElement('style');
  205. style.textContent = `
  206. #${GM_CONFIG_ID}_wrapper { font-family: Arial, sans-serif; }
  207. #${GM_CONFIG_ID}_header { background-color: #4a4a4a; color: white; padding: 10px; font-size: 1.2em; margin-bottom: 10px; }
  208. .section_header { background-color: #f0f0f0; padding: 8px; margin-top: 15px; margin-bottom: 5px; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; font-weight: bold; color: #333; }
  209. .config_var { margin: 10px 15px; padding: 8px 0; border-bottom: 1px solid #eee; display: flex; flex-direction: column; }
  210. .config_var label { display: block; margin-bottom: 5px; color: #555; font-size: 0.9em; font-weight: normal; text-align: left; }
  211. .config_var input { padding: 6px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; margin-left: 0; width: 280px; max-width: 100%; }
  212. #${urlFieldInputId} { width: 100% !important; min-width: 450px !important; }
  213. .config_var input[type="checkbox"] { width: auto !important; margin-right: auto; align-self: flex-start; }
  214. #${GM_CONFIG_ID}_buttons_holder { padding: 15px; text-align: right; border-top: 1px solid #ddd; background-color: #f9f9f9; }
  215. #${GM_CONFIG_ID}_saveBtn, #${GM_CONFIG_ID}_resetBtn, #${GM_CONFIG_ID}_closeBtn { padding: 8px 15px; margin-left: 10px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
  216. #${GM_CONFIG_ID}_saveBtn { background-color: #4CAF50; color: white; }
  217. #${GM_CONFIG_ID}_resetBtn { background-color: #f44336; color: white; }
  218. #${GM_CONFIG_ID}_closeBtn { background-color: #bbb; color: black; }
  219. `;
  220. doc.head.appendChild(style);
  221. const firstInput = doc.querySelector('input[type="text"], input[type="number"], input[type="checkbox"]');
  222. if (firstInput) { firstInput.focus(); }
  223. },
  224. 'save': function() {
  225. consoleLog("Настройки сохранены.");
  226. alert("Настройки сохранены! Некоторые изменения (URL, автозапуск) могут потребовать перезагрузки или вступят в силу при следующей проверке.");
  227. setupAutoStart();
  228. },
  229. 'reset': function() {
  230. consoleLog("Настройки сброшены.");
  231. alert("Настройки сброшены! Перезагрузите страницу.");
  232. setupAutoStart();
  233. }
  234. };
  235.  
  236. let gmConfigInitialized = false;
  237. try { /* ... (GM_config.init - no changes from v2.3.2) ... */
  238. if (typeof GM_config !== 'undefined' && typeof GM_info !== 'undefined') {
  239. GM_config.init({
  240. 'id': GM_CONFIG_ID, 'title': `Настройки Telegram Scraper v${GM_info.script.version}`,
  241. 'fields': configFields, 'events': configEventHandlers,
  242. 'frameStyle': { width: '1000px', height: '75vh', minHeight: '500px', border: '1px solid rgb(0, 0, 0)', margin: '0px', maxHeight: '95%', maxWidth: '95%', opacity: '1', overflow: 'auto', padding: '0px', position: 'fixed', zIndex: '9999' }
  243. });
  244. gmConfigInitialized = true;
  245. consoleLog("GM_config инициализирован.");
  246. } else {
  247. if (typeof GM_config === 'undefined') consoleLog("GM_config не определен.", true);
  248. if (typeof GM_info === 'undefined') consoleLog("GM_info не определен.", true);
  249. }
  250. } catch (e) {
  251. consoleLog("Ошибка инициализации GM_config: " + e, true);
  252. alert("Ошибка инициализации GM_config.");
  253. }
  254.  
  255. function getConfigValue(key, defaultValue) { /* ... (no changes from v2.3.2) ... */
  256. if (gmConfigInitialized && typeof GM_config.get === 'function' && (typeof GM_config.isInit === 'undefined' || GM_config.isInit) ) {
  257. try {
  258. const val = GM_config.get(key);
  259. return typeof val !== 'undefined' ? val : defaultValue;
  260. } catch (e) {
  261. consoleLog(`Ошибка при вызове GM_config.get('${key}'): ${e}. Используется значение по умолчанию.`, true);
  262. const field = configFields[key];
  263. return field && typeof field.default !== 'undefined' ? field.default : defaultValue;
  264. }
  265. }
  266. const field = configFields[key];
  267. return field && typeof field.default !== 'undefined' ? field.default : defaultValue;
  268. }
  269. function getRandomizedInterval(baseInterval, randomnessFactorKey = 'RANDOMNESS_FACTOR_MAJOR') { /* ... (no changes from v2.3.2) ... */
  270. const defaultFactor = configFields[randomnessFactorKey] ? configFields[randomnessFactorKey].default : 0.3; // This default is for the factor itself if key is missing
  271. const factor = getConfigValue(randomnessFactorKey, defaultFactor); // getConfigValue will use its own default from configFields if key exists
  272. const delta = baseInterval * factor * (Math.random() - 0.5) * 2;
  273. return Math.max(50, Math.round(baseInterval + delta));
  274. }
  275.  
  276. async function checkAndRunAutoScrape() { /* ... (no changes from v2.3.2) ... */
  277. if (!gmConfigInitialized || (typeof GM_config !== 'undefined' && typeof GM_config.isInit !== 'undefined' && !GM_config.isInit) ) {
  278. consoleLog("[AutoStart] GM_config еще не готов для проверки автозапуска.");
  279. return;
  280. }
  281. if (!getConfigValue('AUTO_START_ENABLED', false)) { return; }
  282. if (isScrapingSingle || isMultiChannelScrapingActive) { return; }
  283. const scheduledTimeStr = getConfigValue('AUTO_START_TIME', '10:00');
  284. const parts = scheduledTimeStr.split(':');
  285. if (parts.length !== 2) { consoleLog(`[AutoStart] Неверный формат времени: ${scheduledTimeStr}.`, true); return; }
  286. const scheduledHour = parseInt(parts[0], 10);
  287. const scheduledMinute = parseInt(parts[1], 10);
  288. if (isNaN(scheduledHour) || isNaN(scheduledMinute) || scheduledHour < 0 || scheduledHour > 23 || scheduledMinute < 0 || scheduledMinute > 59) {
  289. consoleLog(`[AutoStart] Неверные значения времени: ${scheduledTimeStr}.`, true); return;
  290. }
  291. const now = new Date();
  292. const todayStr = now.toISOString().split('T')[0];
  293. const lastRunDate = GM_getValue(LAST_AUTO_SCRAPE_DATE_KEY, null);
  294. if (lastRunDate === todayStr) { return; }
  295. if (now.getHours() === scheduledHour && now.getMinutes() === scheduledMinute) {
  296. consoleLog(`[AutoStart] Наступило время для автоматического запуска (${scheduledTimeStr})!`);
  297. updateStatusForConsole(`Автозапуск в ${scheduledTimeStr}...`);
  298. GM_setValue(LAST_AUTO_SCRAPE_DATE_KEY, todayStr);
  299. await startMultiChannelScrapeMenu(true);
  300. }
  301. }
  302. function setupAutoStart() { /* ... (no changes from v2.3.2) ... */
  303. if (autoStartCheckInterval) { clearInterval(autoStartCheckInterval); autoStartCheckInterval = null; }
  304. if (getConfigValue('AUTO_START_ENABLED', false)) {
  305. consoleLog("[AutoStart] Автозапуск включен. Проверка времени каждую минуту.");
  306. checkAndRunAutoScrape();
  307. autoStartCheckInterval = setInterval(checkAndRunAutoScrape, 60000);
  308. } else {
  309. consoleLog("[AutoStart] Автозапуск выключен.");
  310. }
  311. }
  312.  
  313. // --- CORE SCRAPING FUNCTIONS ---
  314. // (isTargetChannelActive, parseTimestampFromBubble, extractDataFromMessageElement, sendToN8N, processCurrentMessages, tryScrollUp, scrollToBottom, scrapingLoopSingleChannel, scrapeSingleChannelProcess)
  315. // Definitions are the same as in v2.3.2
  316. function isTargetChannelActive() {
  317. if (!currentScrapingChannelInfo || !currentScrapingChannelInfo.id) { return false; }
  318. const chatInfoContainer = document.querySelector('#column-center .chat.active .sidebar-header .chat-info');
  319. if (!chatInfoContainer) { return false; }
  320. const avatarElement = chatInfoContainer.querySelector('.avatar[data-peer-id]');
  321. if (avatarElement && avatarElement.dataset && avatarElement.dataset.peerId) {
  322. const displayedPeerId = avatarElement.dataset.peerId;
  323. if (displayedPeerId === currentScrapingChannelInfo.id) {
  324. consoleLog(`[isTargetActive] Channel "${currentScrapingChannelInfo.name}" (ID: ${currentScrapingChannelInfo.id}) IS ACTIVE.`);
  325. return true;
  326. }
  327. }
  328. return false;
  329. }
  330. function parseTimestampFromBubble(bubbleElement) {
  331. if (bubbleElement && bubbleElement.dataset && bubbleElement.dataset.timestamp) {
  332. return parseInt(bubbleElement.dataset.timestamp, 10) * 1000;
  333. }
  334. return null;
  335. }
  336. function extractDataFromMessageElement(messageElement) {
  337. const channelNameForSource = currentScrapingChannelInfo ? currentScrapingChannelInfo.name : 'unknown_channel';
  338. const data = {
  339. title: '', text: '', link: null, pubDate: null,
  340. source: `t.me/${channelNameForSource.replace('@','')}`,
  341. messageId: null, rawHtmlContent: messageElement.innerHTML
  342. };
  343. const parentBubble = messageElement.closest('.bubble.channel-post');
  344. if (!parentBubble) { consoleLog(`[Extractor] Parent bubble not found: ${messageElement.textContent.substring(0,50)}...`, true); return null; }
  345. data.messageId = parentBubble.dataset.mid;
  346. if (!data.messageId) { consoleLog(`[Extractor] Message ID not found: ${parentBubble.outerHTML.substring(0,100)}...`, true); return null; }
  347. const timestamp = parseTimestampFromBubble(parentBubble);
  348. if (!timestamp) { consoleLog(`[Extractor] Timestamp not parsed for ID ${data.messageId} in ${channelNameForSource}`, true); return null; }
  349. data.pubDate = new Date(timestamp).toISOString();
  350. const oldestAllowedDate = new Date();
  351. oldestAllowedDate.setHours(oldestAllowedDate.getHours() - getConfigValue('MAX_MESSAGE_AGE_HOURS', 24));
  352. if (new Date(timestamp) < oldestAllowedDate) {
  353. consoleLog(`[Extractor] Msg ID ${data.messageId} (PubDate: ${data.pubDate}) in ${channelNameForSource} OLDER than ${getConfigValue('MAX_MESSAGE_AGE_HOURS', 24)} hours. STOP_SCROLLING.`);
  354. return 'STOP_SCROLLING';
  355. }
  356. const strongElements = Array.from(messageElement.querySelectorAll('strong'));
  357. if (strongElements.length > 0) {
  358. const firstStrong = strongElements.find(s => {
  359. const anchor = s.closest('a');
  360. return !anchor || !(anchor.href.includes(`/${channelNameForSource.replace('@','')}`) || anchor.href.includes(`/${channelNameForSource}`));
  361. });
  362. if (firstStrong) data.title = firstStrong.innerText.trim();
  363. }
  364. let fullText = '';
  365. const channelNamePartForLinkComparison = channelNameForSource.replace('@','');
  366. messageElement.childNodes.forEach(node => {
  367. if (node.nodeType === Node.TEXT_NODE) { fullText += node.textContent; }
  368. else if (node.nodeType === Node.ELEMENT_NODE) {
  369. if (node.tagName === 'A' && node.classList.contains('anchor-url')) {
  370. fullText += node.innerText;
  371. if (!data.link && node.href && node.target === '_blank' && !node.href.startsWith('https://t.me/')) data.link = node.href;
  372. }
  373. else if (node.tagName !== 'STRONG' || (data.title && !node.innerText.trim().startsWith(data.title) && !data.title.includes(node.innerText.trim()))) {
  374. const isCustomEmoji = node.matches && (node.matches('img.custom-emoji') || node.matches('custom-emoji-element') || node.querySelector('img.custom-emoji'));
  375. const isSticker = node.matches && (node.matches('.media-sticker-wrapper') || node.matches('tg-sticker'));
  376. const isReactions = node.matches && (node.matches('reactions-element') || node.classList.contains('reactions'));
  377. let isChannelSignatureLink = false;
  378. if (node.tagName === 'A' && node.href) {
  379. const hrefLower = node.href.toLowerCase();
  380. if (hrefLower.includes(`t.me/${channelNamePartForLinkComparison.toLowerCase()}`) || hrefLower.includes(`/${channelNamePartForLinkComparison.toLowerCase()}`)) {
  381. if (node.innerText.toLowerCase().includes(channelNamePartForLinkComparison.toLowerCase())) isChannelSignatureLink = true;
  382. }
  383. }
  384. if (!isChannelSignatureLink && node.querySelector(`a[href*="/${channelNamePartForLinkComparison}"]`)) {
  385. const nestedLink = node.querySelector(`a[href*="/${channelNamePartForLinkComparison}"]`);
  386. if (nestedLink.innerText.toLowerCase().includes(channelNamePartForLinkComparison.toLowerCase())) isChannelSignatureLink = true;
  387. }
  388. if (!isCustomEmoji && !isSticker && !isReactions && !isChannelSignatureLink) fullText += node.innerText || node.textContent;
  389. }
  390. }
  391. });
  392. data.text = fullText.replace(/\s+/g, ' ').trim();
  393. if (!data.title && data.text) data.title = data.text.substring(0, 120) + (data.text.length > 120 ? '...' : '');
  394. if (data.title && data.text.toLowerCase().startsWith(data.title.toLowerCase())) data.text = data.text.substring(data.title.length).trim();
  395. return data;
  396. }
  397. function sendToN8N(payload) {
  398. const n8nWebhookUrl = getConfigValue('N8N_WEBHOOK_URL', '');
  399. if (!n8nWebhookUrl) { updateStatusForConsole('N8N URL не настроен!', true); return; }
  400. const channelName = currentScrapingChannelInfo ? currentScrapingChannelInfo.name : 'N/A';
  401. const channelId = currentScrapingChannelInfo ? currentScrapingChannelInfo.id : 'N/A';
  402. updateStatusForConsole(`Отправка ID ${payload.messageId} (Канал: ${channelName} [${channelId}], Date: ${payload.pubDate})...`);
  403. GM_xmlhttpRequest({
  404. method: "POST", url: n8nWebhookUrl, data: JSON.stringify(payload), headers: { "Content-Type": "application/json" },
  405. onload: function(response) { updateStatusForConsole(`n8n ответ для ID ${payload.messageId}: ${response.status}`); consoleLog(`[Sender] N8N Response for ID ${payload.messageId}: ${response.status} ${response.responseText.substring(0,100)}`); },
  406. onerror: function(response) { updateStatusForConsole(`n8n ошибка для ID ${payload.messageId}: ${response.status}`, true); consoleLog(`[Sender] N8N Error for ID ${payload.messageId}: ${response.status} ${response.responseText.substring(0,100)}`, true); }
  407. });
  408. }
  409. async function processCurrentMessages() {
  410. if (!isScrapingSingle && !isMultiChannelScrapingActive) return { foundNew: false, stopScrolling: false };
  411. if (!currentScrapingChannelInfo) { consoleLog("processCurrentMessages: currentScrapingChannelInfo is not set.", true); return { foundNew: false, stopScrolling: true, error: "Канал не установлен" };}
  412. if (!isTargetChannelActive()) { updateStatusForConsole(`Канал ${currentScrapingChannelInfo.name} не активен (process).`, true); return { foundNew: false, stopScrolling: true, error: `Канал ${currentScrapingChannelInfo.name} не активен` }; }
  413. updateStatusForConsole(`Поиск в ${currentScrapingChannelInfo.name}...`);
  414. const messageElements = document.querySelectorAll('.bubble.channel-post .message span.translatable-message, .bubble.channel-post .text-content');
  415. let foundNew = false; let stopDueToAge = false;
  416. for (let i = messageElements.length - 1; i >= 0; i--) {
  417. if (!isScrapingSingle && !isMultiChannelScrapingActive) break;
  418. const el = messageElements[i]; const parentBubble = el.closest('.bubble.channel-post'); const msgId = parentBubble ? parentBubble.dataset.mid : null;
  419. if (msgId) {
  420. const articleData = extractDataFromMessageElement(el);
  421. if (articleData === 'STOP_SCROLLING') { stopDueToAge = true; const ts = parentBubble?.dataset.timestamp ? new Date(parseInt(parentBubble.dataset.timestamp,10)*1000).toISOString() : 'N/A'; updateStatusForConsole(`Старые сообщения (ID: ${msgId}, Date: ${ts}). Стоп.`); break; }
  422. if (articleData && articleData.title && (articleData.text || articleData.link)) {
  423. consoleLog(`[Proc] ID ${msgId} (${articleData.pubDate.substring(11,19)}) к отправке.`); sendToN8N(articleData); foundNew = true;
  424. await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('BASE_SEND_DELAY_MS', 1000), 'RANDOMNESS_FACTOR_MINOR')));
  425. } else if (articleData) { consoleLog(`[Proc] ID ${msgId} пропущено (нет данных).`); }
  426. else { consoleLog(`[Proc] ID ${msgId} ошибка извлечения.`, true); }
  427. }
  428. }
  429. return { foundNew, stopScrolling: stopDueToAge };
  430. }
  431. async function tryScrollUp() {
  432. if (!isScrapingSingle && !isMultiChannelScrapingActive) return;
  433. await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('BASE_SCROLL_ACTION_PAUSE_MS', 300), 'RANDOMNESS_FACTOR_MINOR')));
  434. updateStatusForConsole('Скролл вверх...');
  435. const messageBubbles = document.querySelectorAll('.bubbles-inner .bubble.channel-post');
  436. if (messageBubbles.length > 0) {
  437. const topBubble = messageBubbles[0]; if (typeof topBubble.tabIndex === 'undefined' || topBubble.tabIndex === -1) topBubble.tabIndex = -1;
  438. try {
  439. consoleLog(`Скролл к верхнему ID: ${topBubble.dataset.mid} (scrollIntoView)`); topBubble.scrollIntoView({ behavior: 'auto', block: 'start' });
  440. await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('BASE_SCROLL_ACTION_PAUSE_MS', 300), 'RANDOMNESS_FACTOR_MINOR')));
  441. if (getConfigValue('USE_FOCUS_IN_SCROLL_UP', false)) { consoleLog(`Фокус на верхний ID: ${topBubble.dataset.mid}`); topBubble.focus({ preventScroll: true }); }
  442. await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('BASE_SCROLL_PAUSE_MS', 5000))));
  443. } catch (e) {
  444. consoleLog(`Ошибка scrollIntoView/focus: ${e.message}`, true); updateStatusForConsole('Ошибка скролла вверх. Стандартный метод...', true);
  445. const scrollArea = document.querySelector('div.bubbles-inner')?.parentElement || document.querySelector('.scrollable-y.chat-history-list') || document.querySelector('.bubbles > .scrollable-y');
  446. if (scrollArea) { scrollArea.scrollTop = 0; scrollArea.dispatchEvent(new WheelEvent('wheel', { deltaY: -1000, bubbles: true, cancelable: true })); await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('BASE_SCROLL_PAUSE_MS', 5000))));}
  447. }
  448. } else {
  449. updateStatusForConsole('Нет сообщений для скролла вверх. Стандартный метод.');
  450. const scrollArea = document.querySelector('div.bubbles-inner')?.parentElement || document.querySelector('.scrollable-y.chat-history-list') || document.querySelector('.bubbles > .scrollable-y');
  451. if (scrollArea) { scrollArea.scrollTop = 0; scrollArea.dispatchEvent(new WheelEvent('wheel', { deltaY: -1000, bubbles: true, cancelable: true })); await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('BASE_SCROLL_PAUSE_MS', 5000))));}
  452. else { updateStatusForConsole('Нет области скролла и нет сообщений.', true); }
  453. }
  454. }
  455. async function scrollToBottom() {
  456. updateStatusForConsole('Прокрутка к последним сообщениям...');
  457. const scrollableArea = document.querySelector('div.bubbles-inner')?.parentElement || document.querySelector('.scrollable-y.chat-history-list') || document.querySelector('.bubbles > .scrollable-y');
  458. if (!scrollableArea) { updateStatusForConsole('Ошибка: Не найдена область для прокрутки вниз.', true); return false; }
  459. let goToBottomButton; let clicksMade = 0; const maxClicks = getConfigValue('MAX_GO_TO_BOTTOM_CLICKS', 3);
  460. await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('BASE_SCROLL_ACTION_PAUSE_MS', 300), 'RANDOMNESS_FACTOR_MINOR')));
  461. while (clicksMade < maxClicks) {
  462. if (!isScrapingSingle && !isMultiChannelScrapingActive && clicksMade > 0) { updateStatusForConsole('Прокрутка вниз прервана.'); return false; }
  463. goToBottomButton = document.querySelector('.bubbles-go-down.chat-secondary-button:not(.is-hidden):not([style*="display: none"])');
  464. const badge = goToBottomButton ? goToBottomButton.querySelector('.badge:not(.is-badge-empty)') : null;
  465. if (goToBottomButton && badge && typeof goToBottomButton.click === 'function') {
  466. const unreadCountText = badge.textContent; updateStatusForConsole(`Клик по кнопке "вниз" (${unreadCountText || 'несколько'} непрочитанных)...`);
  467. consoleLog(`[ScrollToBottom] Clicking "go to bottom" button (unread: ${unreadCountText}). Click ${clicksMade + 1}`);
  468. goToBottomButton.click(); clicksMade++;
  469. await new Promise(resolve => setTimeout(resolve, getRandomizedInterval(getConfigValue('BASE_SCROLL_BOTTOM_CLICK_PAUSE_MS', 2500))));
  470. } else { consoleLog('[ScrollToBottom] "Go to bottom" button with counter not found or empty.'); break; }
  471. }
  472. updateStatusForConsole('Программная прокрутка вниз...'); let prevScrollHeight = 0; const scrollIterations = getConfigValue('SCROLL_BOTTOM_PROGRAMMATIC_ITERATIONS', 3);
  473. for (let i = 0; i < scrollIterations; i++) {
  474. if (!isScrapingSingle && !isMultiChannelScrapingActive) { updateStatusForConsole('Прокрутка вниз прервана.'); return false; }
  475. prevScrollHeight = scrollableArea.scrollHeight; scrollableArea.scrollTop = scrollableArea.scrollHeight;
  476. updateStatusForConsole(`Прокрутка вниз... (итерация ${i + 1}/${scrollIterations})`);
  477. await new Promise(resolve => setTimeout(resolve, getRandomizedInterval(getConfigValue('BASE_SCROLL_BOTTOM_PROG_PAUSE_MS', 700), 'RANDOMNESS_FACTOR_MINOR')));
  478. if (i > 0 && scrollableArea.scrollHeight - prevScrollHeight < 50) { consoleLog('[ScrollToBottom] Scroll height changed minimally.'); break; }
  479. }
  480. const lastMessageGroup = document.querySelector('.bubbles-inner .bubbles-group-last');
  481. if (lastMessageGroup) {
  482. consoleLog('[ScrollToBottom] Found .bubbles-group-last, scrolling to it.'); updateStatusForConsole('Точная прокрутка к последней группе...');
  483. lastMessageGroup.scrollIntoView({ behavior: 'auto', block: 'end' });
  484. await new Promise(resolve => setTimeout(resolve, getRandomizedInterval(getConfigValue('BASE_SCROLL_BOTTOM_PROG_PAUSE_MS', 700) / 2, 'RANDOMNESS_FACTOR_MINOR')));
  485. } else { consoleLog('[ScrollToBottom] .bubbles-group-last not found.'); }
  486. goToBottomButton = document.querySelector('.bubbles-go-down.chat-secondary-button:not(.is-hidden):not([style*="display: none"])');
  487. if (goToBottomButton && typeof goToBottomButton.click === 'function' && clicksMade < maxClicks) {
  488. const finalBadge = goToBottomButton.querySelector('.badge:not(.is-badge-empty)');
  489. if (!finalBadge) { consoleLog('[ScrollToBottom] "Go to bottom" button (no counter) is active, final click.'); updateStatusForConsole('Финальный клик по кнопке "вниз"...'); goToBottomButton.click(); await new Promise(resolve => setTimeout(resolve, getRandomizedInterval(getConfigValue('BASE_SCROLL_BOTTOM_CLICK_PAUSE_MS', 2500) / 2))); }
  490. }
  491. updateStatusForConsole('Прокрутка к последним сообщениям завершена.'); return true;
  492. }
  493. async function scrapingLoopSingleChannel() {
  494. if (!isScrapingSingle) { consoleLog(`[Loop-${currentScrapingChannelInfo.name}] Остановлен (isScrapingSingle=false).`); return; }
  495. if (isMultiChannelScrapingActive && !isScrapingSingle) { consoleLog(`[Loop-${currentScrapingChannelInfo.name}] Остановлен (multi active, single false).`); return; }
  496. await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('BASE_SCROLL_ACTION_PAUSE_MS', 300), 'RANDOMNESS_FACTOR_MINOR')));
  497. const { foundNew, stopScrolling, error } = await processCurrentMessages();
  498. if (error) { updateStatusForConsole(error + `. Прерываю для ${currentScrapingChannelInfo.name}.`, true); return; }
  499. if (stopScrolling) { updateStatusForConsole(`Лимит по дате для ${currentScrapingChannelInfo.name}. Завершаю.`); return; }
  500. if (foundNew) { consecutiveScrollsWithoutNewFound = 0; }
  501. else { consecutiveScrollsWithoutNewFound++; consoleLog(`[Loop-${currentScrapingChannelInfo.name}] Ничего нового. Счетчик: ${consecutiveScrollsWithoutNewFound}`);}
  502. if (consecutiveScrollsWithoutNewFound >= getConfigValue('CONSECUTIVE_SCROLLS_LIMIT', 5)) {
  503. updateStatusForConsole(`Нет новых сообщений для ${currentScrapingChannelInfo.name} после ${getConfigValue('CONSECUTIVE_SCROLLS_LIMIT', 5)} прокруток. Завершаю.`); return;
  504. }
  505. await tryScrollUp();
  506. if (isScrapingSingle) {
  507. const baseNextInterval = !foundNew ? getConfigValue('BASE_SCRAPE_INTERVAL_MS', 30000) : getConfigValue('BASE_SCRAPE_INTERVAL_MS', 30000) / 2;
  508. await new Promise(r => setTimeout(r, getRandomizedInterval(baseNextInterval)));
  509. if (isScrapingSingle) await scrapingLoopSingleChannel();
  510. }
  511. }
  512. async function scrapeSingleChannelProcess(channelInfoObject) {
  513. if (!channelInfoObject || !channelInfoObject.id || !channelInfoObject.name) { consoleLog("Ошибка: Некорректные данные канала в scrapeSingleChannelProcess", true); return false; }
  514. if (!isScrapingSingle && !isMultiChannelScrapingActive) { consoleLog(`scrapeSingleChannelProcess для ${channelInfoObject.name} не может быть запущен (флаги).`); return false; }
  515. currentScrapingChannelInfo = channelInfoObject;
  516. consoleLog(`--- Начало скрапинга канала: ${currentScrapingChannelInfo.name} (ID: ${currentScrapingChannelInfo.id}) ---`);
  517. updateStatusForConsole(`Скрапинг: ${currentScrapingChannelInfo.name}`);
  518. const targetHashForNavigation = `#${currentScrapingChannelInfo.name}`;
  519. let navigationNeeded = true;
  520. const chatInfoContainerInitial = document.querySelector('#column-center .chat.active .sidebar-header .chat-info');
  521. let initialDisplayedPeerId = null;
  522. if (chatInfoContainerInitial) {
  523. const avatarElementInitial = chatInfoContainerInitial.querySelector('.avatar[data-peer-id]');
  524. if (avatarElementInitial) { initialDisplayedPeerId = avatarElementInitial.dataset.peerId; }
  525. }
  526. if (initialDisplayedPeerId === currentScrapingChannelInfo.id) { consoleLog(`[Nav] Уже на канале ${currentScrapingChannelInfo.name} (peerId совпадает).`); navigationNeeded = false; }
  527. else if (window.location.hash.toLowerCase() === targetHashForNavigation.toLowerCase() && initialDisplayedPeerId) { consoleLog(`[Nav] URL hash is ${targetHashForNavigation} or peerId (${initialDisplayedPeerId}) present, but expecting ${currentScrapingChannelInfo.id}. Will wait for peerId activation.`); navigationNeeded = false; }
  528. if (navigationNeeded) {
  529. consoleLog(`Перехожу на канал ${targetHashForNavigation}...`); window.location.hash = targetHashForNavigation;
  530. await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('NAVIGATION_INITIATION_PAUSE_MS', 2500), 'RANDOMNESS_FACTOR_MAJOR')));
  531. }
  532. let activationAttempts = 0; const maxActivationAttempts = getConfigValue('MAX_CHANNEL_ACTIVATION_ATTEMPTS', 25);
  533. consoleLog(`Ожидание активации канала ${currentScrapingChannelInfo.name} (ID: ${currentScrapingChannelInfo.id}) по peer-id...`);
  534. while (activationAttempts < maxActivationAttempts) {
  535. if (!isScrapingSingle && !isMultiChannelScrapingActive) { consoleLog("Остановка во время ожидания активации канала."); return false; }
  536. if (isTargetChannelActive()) break;
  537. activationAttempts++; updateStatusForConsole(`Ожидание ${currentScrapingChannelInfo.name} (${activationAttempts}/${maxActivationAttempts})`);
  538. await new Promise(r => setTimeout(r, getRandomizedInterval(getConfigValue('CHANNEL_ACTIVATION_ATTEMPT_PAUSE_MS', 700), 'RANDOMNESS_FACTOR_MINOR')));
  539. }
  540. if (!isTargetChannelActive()) { updateStatusForConsole(`Не удалось активировать ${currentScrapingChannelInfo.name} (ID: ${currentScrapingChannelInfo.id}) по peer-id. Пропускаю.`, true); return false; }
  541. consoleLog(`Канал ${currentScrapingChannelInfo.name} активен. Прокрутка вниз.`);
  542. const scrolledToBottom = await scrollToBottom();
  543. if (!scrolledToBottom) { if (isScrapingSingle || isMultiChannelScrapingActive) { updateStatusForConsole(`Ошибка прокрутки вниз для ${currentScrapingChannelInfo.name}.`, true); } return false; }
  544. if (!isScrapingSingle && !isMultiChannelScrapingActive) { consoleLog("Остановка после прокрутки вниз."); return false;}
  545. updateStatusForConsole(`Скрапинг вверх для ${currentScrapingChannelInfo.name}...`);
  546. consecutiveScrollsWithoutNewFound = 0; await scrapingLoopSingleChannel();
  547. consoleLog(`--- Скрапинг канала ${currentScrapingChannelInfo.name} завершен/остановлен ---`); return true;
  548. }
  549.  
  550. // --- MENU COMMAND HANDLERS ---
  551. function shuffleArray(a) { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; }
  552.  
  553. async function startSingleChannelScrapeMenu() {
  554. consoleLog("Команда 'Scrape Current Channel' вызвана. / 'Scrape Current Channel' command called.");
  555. if (isScrapingSingle || isMultiChannelScrapingActive) {
  556. alert("Скрапинг уже запущен. / Scraping is already running.");
  557. consoleLog("Скрапинг уже запущен.", true); return;
  558. }
  559. let displayedPeerId = null;
  560. const chatInfoContainer = document.querySelector('#column-center .chat.active .sidebar-header .chat-info');
  561. if (chatInfoContainer) { const avatarElement = chatInfoContainer.querySelector('.avatar[data-peer-id]'); if (avatarElement && avatarElement.dataset && avatarElement.dataset.peerId) displayedPeerId = avatarElement.dataset.peerId; }
  562. let channelInfoToScrape = null;
  563. if (displayedPeerId) {
  564. channelInfoToScrape = TARGET_CHANNELS_DATA_ORIGINAL.find(ch => ch.id === displayedPeerId);
  565. if (channelInfoToScrape) consoleLog(`[startSingle] Канал определен по peer-id: ${channelInfoToScrape.name}`);
  566. else consoleLog(`[startSingle] Peer-id ${displayedPeerId} не найден в TARGET_CHANNELS_DATA.`);
  567. } else consoleLog(`[startSingle] Не удалось получить peer-id.`);
  568. if (!channelInfoToScrape) {
  569. let hash = window.location.hash.substring(1);
  570. if (hash) {
  571. const queryParamIndex = hash.indexOf('?'); if (queryParamIndex !== -1) hash = hash.substring(0, queryParamIndex);
  572. channelInfoToScrape = TARGET_CHANNELS_DATA_ORIGINAL.find(ch => ch.id === hash);
  573. if (!channelInfoToScrape) { let nameToCompare = hash; if (!hash.startsWith('@') && isNaN(parseInt(hash))) nameToCompare = '@' + hash; channelInfoToScrape = TARGET_CHANNELS_DATA_ORIGINAL.find(ch => ch.name.toLowerCase() === nameToCompare.toLowerCase()); }
  574. if (channelInfoToScrape) consoleLog(`[startSingle] Канал определен по hash "${hash}": ${channelInfoToScrape.name}`);
  575. else consoleLog(`[startSingle] Канал не определен по hash "${hash}".`);
  576. }
  577. }
  578. if (!channelInfoToScrape) { alert("Не удалось определить текущий канал."); consoleLog("Не удалось определить текущий канал.", true); return; }
  579. isScrapingSingle = true; consoleLog(`--- Начало ОДИНОЧНОЙ сессии для ${channelInfoToScrape.name} ---`);
  580. alert(`Начинаю скрапинг текущего канала: ${channelInfoToScrape.name}.`);
  581. await scrapeSingleChannelProcess(channelInfoToScrape);
  582. isScrapingSingle = false;
  583. if (!isMultiChannelScrapingActive) { updateStatusForConsole("Скрапинг текущего канала завершен."); consoleLog("--- ОДИНОЧНАЯ сессия скрапинга завершена ---"); alert(`Скрапинг канала ${channelInfoToScrape.name} завершен.`); }
  584. currentScrapingChannelInfo = null;
  585. }
  586. async function startMultiChannelScrapeMenu(isAutoStart = false) {
  587. if (!isAutoStart) {
  588. consoleLog("Команда 'Scrape All Listed Channels' вызвана. / 'Scrape All Listed Channels' command called.");
  589. if (isScrapingSingle || isMultiChannelScrapingActive) { alert("Скрапинг уже запущен. / Scraping is already running."); consoleLog("Скрапинг уже запущен.", true); return; }
  590. if (!confirm(`Начать скрапинг ${TARGET_CHANNELS_DATA_ORIGINAL.length} каналов? / Start scraping ${TARGET_CHANNELS_DATA_ORIGINAL.length} channels?`)) { consoleLog("Мульти-скрапинг отменен. / Multi-scrape cancelled."); return; }
  591. } else {
  592. if (isScrapingSingle || isMultiChannelScrapingActive) { consoleLog("[AutoStart] Скрапинг уже запущен, автозапуск пропущен. / Scraping in progress, auto-start skipped."); return; }
  593. consoleLog("[AutoStart] Запуск мульти-скрапинга по расписанию. / Starting scheduled multi-scrape.");
  594. }
  595. isMultiChannelScrapingActive = true; currentChannelIndex = 0;
  596. if (getConfigValue('RANDOMIZE_CHANNEL_ORDER', true)) { consoleLog("Перемешивание порядка каналов... / Randomizing channel order..."); currentTargetChannels = shuffleArray([...TARGET_CHANNELS_DATA_ORIGINAL]); }
  597. else { currentTargetChannels = [...TARGET_CHANNELS_DATA_ORIGINAL]; }
  598. consoleLog("--- Начало МУЛЬТИ-СКРАПИНГА --- / --- Starting MULTI-CHANNEL SCRAPING ---");
  599. if (!isAutoStart) alert("Начинаю скрапинг всех каналов. / Starting scrape of all channels.");
  600. while (currentChannelIndex < currentTargetChannels.length && isMultiChannelScrapingActive) {
  601. isScrapingSingle = true; const channelInfo = currentTargetChannels[currentChannelIndex];
  602. updateStatusForConsole(`[${currentChannelIndex + 1}/${currentTargetChannels.length}] Запуск для: ${channelInfo.name} / Starting for: ${channelInfo.name}`);
  603. const success = await scrapeSingleChannelProcess(channelInfo);
  604. isScrapingSingle = false;
  605. if (!isMultiChannelScrapingActive) { consoleLog("Мульти-скрапинг остановлен. / Multi-scrape stopped."); break; }
  606. if (!success) consoleLog(`Проблема со скрапингом канала ${channelInfo.name}, пропускаю. / Problem scraping ${channelInfo.name}, skipping.`, true);
  607. currentChannelIndex++;
  608. if (currentChannelIndex < currentTargetChannels.length && isMultiChannelScrapingActive) {
  609. const pauseDuration = getRandomizedInterval(getConfigValue('BASE_SCROLL_PAUSE_MS', 5000) * 1.5, 'RANDOMNESS_FACTOR_MAJOR');
  610. updateStatusForConsole(`Пауза ${Math.round(pauseDuration/1000)}с перед ${currentTargetChannels[currentChannelIndex].name} / Pausing ${Math.round(pauseDuration/1000)}s before ${currentTargetChannels[currentChannelIndex].name}`);
  611. await new Promise(r => setTimeout(r, pauseDuration));
  612. }
  613. }
  614. if (isMultiChannelScrapingActive) {
  615. updateStatusForConsole("Скрапинг ВСЕХ каналов завершен. / Scraping of ALL channels finished.");
  616. if (!isAutoStart) alert("Скрапинг всех каналов завершен! / Scraping of all channels finished!");
  617. else consoleLog("[AutoStart] Автоматический сбор завершен. / Auto-scrape finished.");
  618. }
  619. isMultiChannelScrapingActive = false; isScrapingSingle = false; currentScrapingChannelInfo = null;
  620. }
  621. function stopAllScrapingActivitiesMenu() {
  622. consoleLog("Команда 'Stop All Scraping' вызвана. / 'Stop All Scraping' command called.", true);
  623. isScrapingSingle = false; isMultiChannelScrapingActive = false;
  624. updateStatusForConsole('Скрапинг остановлен пользователем. / Scraping stopped by user.'); alert("Все процессы скрапинга остановлены. / All scraping processes stopped.");
  625. }
  626. function toggleAutoStartMenu() {
  627. const currentAutoStart = getConfigValue('AUTO_START_ENABLED', false);
  628. const newAutoStart = !currentAutoStart;
  629. GM_config.set('AUTO_START_ENABLED', newAutoStart); GM_config.save();
  630. alert(`Автозапуск ${newAutoStart ? 'ВКЛЮЧЕН' : 'ВЫКЛЮЧЕН'}. / Auto-start ${newAutoStart ? 'ENABLED' : 'DISABLED'}.`);
  631. consoleLog(`Автозапуск ${newAutoStart ? 'ВКЛЮЧЕН' : 'ВЫКЛЮЧЕН'} через меню. / Auto-start ${newAutoStart ? 'ENABLED' : 'DISABLED'} via menu.`);
  632. setupAutoStart();
  633. }
  634.  
  635. // --- REGISTER MENU COMMANDS ---
  636. if (typeof GM_registerMenuCommand === 'function') {
  637. if (gmConfigInitialized) {
  638. GM_registerMenuCommand("Scrape Current Channel / Собрать с текущего канала", startSingleChannelScrapeMenu, "C");
  639. GM_registerMenuCommand("Scrape All Listed Channels / Собрать со всех каналов", () => startMultiChannelScrapeMenu(false), "A");
  640. GM_registerMenuCommand("Toggle Auto-Start / Вкл/Выкл Автозапуск", toggleAutoStartMenu, "T");
  641. GM_registerMenuCommand("Stop All Scraping / Остановить всё", stopAllScrapingActivitiesMenu, "S");
  642. GM_registerMenuCommand("Script Settings... / Настройки скрипта...", () => GM_config.open(), "O");
  643. consoleLog("Команды меню Tampermonkey зарегистрированы. / Tampermonkey menu commands registered.");
  644. } else {
  645. consoleLog("GM_config не был успешно инициализирован. / GM_config was not successfully initialized.", true); alert("Ошибка: GM_config не инициализирован. / Error: GM_config not initialized.");
  646. GM_registerMenuCommand("Scrape Current Channel / Собрать с текущего канала", startSingleChannelScrapeMenu, "C");
  647. GM_registerMenuCommand("Scrape All Listed Channels / Собрать со всех каналов",() => startMultiChannelScrapeMenu(false), "A");
  648. GM_registerMenuCommand("Stop All Scraping / Остановить всё", stopAllScrapingActivitiesMenu, "S");
  649. consoleLog("Основные команды меню Tampermonkey зарегистрированы. / Basic Tampermonkey menu commands registered.");
  650. }
  651. } else {
  652. consoleLog("GM_registerMenuCommand не доступна. / GM_registerMenuCommand is not available.", true); alert("Tampermonkey API GM_registerMenuCommand не доступно. / Tampermonkey API GM_registerMenuCommand is not available.");
  653. }
  654.  
  655. // Initialize auto-start check after a short delay to ensure GM_config is fully ready.
  656. // Инициализация проверки автозапуска после небольшой задержки, чтобы GM_config был полностью готов.
  657. if (gmConfigInitialized) {
  658. setTimeout(() => {
  659. consoleLog("Первоначальная настройка автозапуска после задержки. / Initial auto-start setup after delay.");
  660. setupAutoStart();
  661. }, 1000); // 1 second delay / Задержка в 1 секунду
  662. }
  663.  
  664. })();
  665. console.log(`[Telegram Scraper v${GM_info.script.version}] Script IIFE execution completed.`);