VK Chat Parser with Google Sheets Integration

Parse lists from all messages (both types in one message) and update Google Sheets

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         VK Chat Parser with Google Sheets Integration
// @namespace    http://tampermonkey.net/
// @version      0.36
// @description  Parse lists from all messages (both types in one message) and update Google Sheets
// @author       Grok
// @match        https://web.vk.me/convo/*
// @grant        GM_xmlhttpRequest
// @connect      script.google.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const originalList = [
        "Асеева Алиса", "Базарсадаева Айлана", "Бальжинимаев Лубсан", "Беина Маргарита",
        "Белоусова Виктория", "Белоусова Ольга", "Буянтуева Номин", "Воробьев Алексей",
        "Воробьева Светлана", "Воронов Артур", "Вторушин Константин", "Геращенко Ангелина",
        "Дашадоржиев Сандан", "Жамбалов Доржо", "Жигжитова Виктория", "Зиновьев Геннадий",
        "Иванов Роман", "Константинова Татьяна", "Лаухин Сергей", "Попов Роман",
        "Раднаева Ольга", "Распопова Мирослава", "Самбуева Сарана", "Сергеев Марк",
        "Славко Валентина", "Степанова Ангелина", "Цыжипова Баярма"
    ];

    const monthNames = [
        "Сентябрь", "Октябрь", "Ноябрь", "Декабрь", "Январь",
        "Февраль", "Март", "Апрель", "Май"
    ];

    function initializeButton() {
        if (document.querySelector('button#parseButton') || !document.body) return;

        const button = document.createElement('button');
        button.id = 'parseButton';
        button.textContent = 'Отправить';
        button.style.position = 'fixed';
        button.style.top = '10px';
        button.style.right = '150px';
        button.style.zIndex = '9999';
        button.style.padding = '10px';
        button.style.backgroundColor = '#4CAF50';
        button.style.color = 'white';
        button.style.border = 'none';
        button.style.borderRadius = '5px';
        button.style.cursor = 'pointer';
        button.style.transition = 'background-color 0.3s';
        document.body.appendChild(button);

        button.addEventListener('click', () => {
            button.disabled = true;
            button.style.backgroundColor = '#f44336';
            button.textContent = 'Найдено 0';
            parseAndUpdate(button).finally(() => {
                button.disabled = false;
                button.style.backgroundColor = '#4CAF50';
                button.textContent = 'Отправить';
            });
        });
        console.log('Кнопка инициализирована');
    }

    document.addEventListener('DOMContentLoaded', initializeButton);
    setInterval(() => {
        if (!document.querySelector('button#parseButton') && document.body) {
            console.log('Периодическая инициализация кнопки');
            initializeButton();
        }
    }, 1000);

    function findClosestMatch(inputName) {
        const inputLower = inputName.toLowerCase().replace(/\.\s*/g, ' ').trim();
        const inputParts = inputLower.split(' ');
        const inputSurname = inputParts[0];
        const inputInitial = inputParts[1] ? inputParts[1][0] : '';

        return originalList.find(original => {
            const originalLower = original.toLowerCase().replace(/\s+/g, ' ');
            const originalParts = originalLower.split(' ');
            const originalSurname = originalParts[0];
            const originalInitial = originalParts[1] ? originalParts[1][0] : '';

            if (originalSurname === 'воробьев' || originalSurname === 'воробьева' ||
                originalSurname === 'белоусова') {
                return originalSurname === inputSurname && originalInitial === inputInitial;
            } else {
                return originalSurname === inputSurname;
            }
        }) || inputName;
    }

    function parseMessageText(htmlContent) {
        const text = htmlContent.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '').trim();
        const lines = text.split('\n').map(line => line.trim()).filter(line => line);
        if (lines.length < 2) return { absent: [], notEating: [] };

        let absentLines = [];
        let notEatingLines = [];
        let currentList = null;

        lines.forEach((line, index) => {
            const lowerLine = line.toLowerCase();

            if (lowerLine.match(/^\d+\/\d+$/) && index === 0) {
                return;
            } else if (lowerLine.includes('нет')) {
                currentList = 'absent';
            } else if (lowerLine.includes('не кушают')) {
                currentList = 'notEating';
            } else if (currentList && line.trim()) {
                const matchedName = findClosestMatch(line);
                if (currentList === 'absent') {
                    absentLines.push(matchedName);
                } else if (currentList === 'notEating') {
                    notEatingLines.push(matchedName);
                }
            }
        });

        return {
            absent: absentLines,
            notEating: notEatingLines,
            absentList: absentLines.length ? '\n' + absentLines.join('\n') : '',
            notEatingList: notEatingLines.length ? '\n' + notEatingLines.join('\n') : ''
        };
    }

    function getDateFromLabel(label, currentDate) {
        const normalizedLabel = label.toLowerCase().trim();
        const date = new Date(currentDate);

        if (normalizedLabel === 'сегодня') {
            // Используем текущую дату
        } else if (normalizedLabel === 'вчера') {
            date.setDate(date.getDate() - 1);
        } else {
            const [dayStr, monthStr] = normalizedLabel.split(' ');
            const day = parseInt(dayStr, 10);
            const monthMap = {
                'января': 0, 'февраля': 1, 'марта': 2, 'апреля': 3,
                'мая': 4, 'июня': 5, 'июля': 6, 'августа': 7,
                'сентября': 8, 'октября': 9, 'ноября': 10, 'декабря': 11
            };
            const month = monthMap[monthStr];
            if (day && month !== undefined) {
                date.setDate(day);
                date.setMonth(month);
                if (month > date.getMonth()) {
                    date.setFullYear(date.getFullYear() - 1);
                }
            }
        }

        const dayStr = String(date.getDate()).padStart(2, '0');
        const monthStr = String(date.getMonth() + 1).padStart(2, '0');
        const year = date.getFullYear();
        return `${dayStr}.${monthStr}.${year}`;
    }

    function updateGoogleSheets(absentList, notEatingList, dateString, sheetName) {
        return new Promise((resolve, reject) => {
            const payload = JSON.stringify({
                sheetName: sheetName,
                absent: absentList,
                notEating: notEatingList,
                date: dateString
            });

            console.log(`Отправляемые данные для ${dateString}:`, payload);

            const apiUrl = 'https://script.google.com/macros/s/AKfycbzSjufiH1WUjKPSLq_k3Un6Q-FB_n6R_dVCrzDlz4Hxg2HOkVg4llng_c-cP7gOmPeO/exec';
            if (typeof GM_xmlhttpRequest !== 'undefined') {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: apiUrl,
                    headers: { 'Content-Type': 'application/json' },
                    data: payload,
                    onload: response => {
                        console.log(`Ответ от API для ${dateString}:`, response.responseText);
                        resolve();
                    },
                    onerror: error => {
                        console.error(`Ошибка GM_xmlhttpRequest для ${dateString}:`, error);
                        reject(error);
                    }
                });
            } else {
                console.warn('GM_xmlhttpRequest не доступен, используем fetch');
                fetch(apiUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: payload,
                    mode: 'no-cors'
                })
                .then(() => {
                    console.log(`Данные отправлены через fetch для ${dateString} (no-cors)`);
                    resolve();
                })
                .catch(error => {
                    console.error(`Ошибка fetch для ${dateString}:`, error);
                    reject(error);
                });
            }
        });
    }

    async function parseAndUpdate(button) {
        const currentDate = new Date(); // Фиксируем дату для примера
        const dateSeparators = document.querySelectorAll('.DateSeparator');
        let hasLists = false;

        if (!dateSeparators.length) {
            console.log('Блоки дат не найдены');
            button.textContent = 'Ошибка';
            button.style.backgroundColor = '#f44336';
            await new Promise(resolve => setTimeout(resolve, 3000)); // Ждём 3 секунды
            return;
        }

        const dataByDate = {};

        dateSeparators.forEach(separator => {
            const label = separator.textContent.trim();
            const dateString = getDateFromLabel(label, currentDate);
            const dateStack = separator.closest('.ConvoHistory__dateStack');
            if (!dateStack) return;

            const convoStacks = dateStack.querySelectorAll('.ConvoStack');
            let absentList = [];
            let notEatingList = [];

            convoStacks.forEach(stack => {
                const messages = stack.querySelectorAll('.ConvoMessage');
                messages.forEach(message => {
                    const content = message.querySelector('.ConvoMessage__text');
                    if (content) {
                        const messageHtml = content.innerHTML;
                        const result = parseMessageText(messageHtml);

                        if (result.absent.length && absentList.length === 0) {
                            absentList = result.absent;
                            console.log(`Отсутствующие (${label}):`, result.absentList);
                        }
                        if (result.notEating.length && notEatingList.length === 0) {
                            notEatingList = result.notEating;
                            console.log(`Не кушают (${label}):`, result.notEatingList);
                        }
                    }

                    const forwardedContainer = message.querySelector('.ConvoMessage__forwardedMessages');
                    if (forwardedContainer) {
                        const forwardedMessages = forwardedContainer.querySelectorAll('.ForwardedMessageNew__text');
                        forwardedMessages.forEach(fwdText => {
                            const fwdHtml = fwdText.innerHTML;
                            const fwdResult = parseMessageText(fwdHtml);

                            if (fwdResult.absent.length && absentList.length === 0) {
                                absentList = fwdResult.absent;
                                console.log(`Отсутствующие (пересылаемое, ${label}):`, fwdResult.absentList);
                            }
                            if (fwdResult.notEating.length && notEatingList.length === 0) {
                                notEatingList = fwdResult.notEating;
                                console.log(`Не кушают (пересылаемое, ${label}):`, fwdResult.notEatingList);
                            }
                        });
                    }
                });
            });

            if (absentList.length || notEatingList.length) {
                dataByDate[dateString] = { absentList, notEatingList };
                hasLists = true;
            }
        });

        const listCount = Object.keys(dataByDate).length; // Количество дней с данными

        if (!hasLists) {
            console.log('Списки не найдены');
            button.textContent = 'Ошибка';
            button.style.backgroundColor = '#f44336';
            await new Promise(resolve => setTimeout(resolve, 3000)); // Ждём 3 секунды
        } else {
            button.textContent = `Найдено ${listCount}`;
            const promises = Object.entries(dataByDate).map(([dateString, { absentList, notEatingList }]) => {
                const date = new Date(dateString.split('.').reverse().join('-'));
                const monthIndex = date.getMonth();
                const adjustedIndex = (monthIndex + 4) % 12; // Сдвиг для учебного года
                const sheetName = monthNames[adjustedIndex];

                console.log(`=== Результаты для ${dateString} ===`);
                console.log('Отсутствующие:', absentList.length ? '\n' + absentList.join('\n') : 'Список не найден');
                console.log('Не кушают:', notEatingList.length ? '\n' + notEatingList.join('\n') : 'Список не найден');

                return updateGoogleSheets(absentList, notEatingList, dateString, sheetName);
            });

            await Promise.all(promises);
        }
    }

    initializeButton();
})();