GetCourse: сбор статистики в Google Таблице
Скрипт для Google Sheets, который собирает статистику рассылок и количество пользователей в сегментах из GetCourse.
Возможности
- Письма — выгрузка статистики рассылок (всего, доставлено, просмотры, клики, отписки, ошибки, запрещено)
- Сегменты — подсчёт пользователей в сегменте по ссылке
- Мульти-аккаунт — поддержка нескольких аккаунтов GetCourse
Установка
- Открыть Google Таблицу
- Расширения → Apps Script
Удалить содержимое файлаCode.gs- Вставить код из раздела Код скрипта
- Сохранить (Ctrl+S)
- Вернуться в таблицу, обновить страницу
- В меню появится вкладка GetCourse
Первый запуск
- GetCourse → Управление аккаунтами → добавить аккаунт (имя, email, пароль, домен вида
https://your-school.getcourse.ru) - GetCourse → Проверить подключение — убедиться, что авторизация работает
- При первом запуске Google попросит разрешения — нажать "Разрешить"
Использование
Статистика рассылок
- В ячейки вписать ID рассылок (число) или ссылки на рассылки
- Выделить эти ячейки
- GetCourse → Письма: обновить выбранные
- В ячейках справа от ID появятся данные (название, всего, доставлено и т.д.), каждая ячейка с примечанием
Количество пользователей в сегменте
- В ячейку вставить ссылку на сегмент (или гиперссылку)
- Выделить ячейку
- GetCourse → Пользователи: кол-во в сегментах
- В ячейке справа — количество, через одну — дата обновления
Код скрипта
/**
* GC: Email Stats
*
* Сбор статистики рассылок GetCourse прямо в Google Таблице.
*
* Установка:
* 1. Расширения → Apps Script → вставить этот код
* 2. Вписать ID или ссылки рассылок в ячейки
* 3. Выделить ячейки → меню GC: Email Stats → Обновить выбранные
*/
// ============================================================
// Меню
// ============================================================
function onOpen() {
var menuName = 'GetCourse';
var accounts = getAccounts();
if (accounts.length > 0) {
var idx = getActiveAccountIndex();
var acc = accounts[idx];
menuName = 'GetCourse [' + (acc.name || acc.email) + ']';
}
SpreadsheetApp.getUi()
.createMenu(menuName)
.addItem('Письма: обновить выбранные', 'updateSelectedRows')
.addItem('Пользователи: кол-во в сегментах', 'updateSegmentCounts')
.addSeparator()
.addItem('Управление аккаунтами', 'showAccountManager')
.addItem('Проверить подключение', 'testConnection')
.addToUi();
}
// ============================================================
// Управление аккаунтами
// ============================================================
function getAccounts() {
var props = PropertiesService.getScriptProperties();
var json = props.getProperty('gc_accounts');
if (!json) return [];
try { return JSON.parse(json); } catch (e) { return []; }
}
function getActiveAccountIndex() {
var props = PropertiesService.getScriptProperties();
var idx = parseInt(props.getProperty('gc_active_account') || '0', 10);
var accounts = getAccounts();
if (idx >= accounts.length) idx = 0;
return idx;
}
function saveAccount(accountData) {
var accounts = getAccounts();
var props = PropertiesService.getScriptProperties();
if (accountData.index !== undefined && accountData.index !== null && accountData.index >= 0 && accountData.index < accounts.length) {
accounts[accountData.index] = {
name: accountData.name,
email: accountData.email,
password: accountData.password,
domain: accountData.domain
};
} else {
accounts.push({
name: accountData.name,
email: accountData.email,
password: accountData.password,
domain: accountData.domain
});
}
props.setProperty('gc_accounts', JSON.stringify(accounts));
if (accounts.length === 1) {
props.setProperty('gc_active_account', '0');
}
return accounts.length - 1;
}
function deleteAccount(index) {
var accounts = getAccounts();
var props = PropertiesService.getScriptProperties();
if (index < 0 || index >= accounts.length) return;
accounts.splice(index, 1);
props.setProperty('gc_accounts', JSON.stringify(accounts));
var activeIdx = getActiveAccountIndex();
if (index === activeIdx) {
props.setProperty('gc_active_account', '0');
CacheService.getScriptCache().remove('gc_session');
} else if (index < activeIdx) {
props.setProperty('gc_active_account', String(activeIdx - 1));
}
}
function setActiveAccount(index) {
var accounts = getAccounts();
if (index < 0 || index >= accounts.length) return;
PropertiesService.getScriptProperties().setProperty('gc_active_account', String(index));
CacheService.getScriptCache().remove('gc_session');
}
function getAccountsForDialog() {
var accounts = getAccounts();
var activeIdx = getActiveAccountIndex();
return { accounts: accounts, activeIndex: activeIdx };
}
function getConfig() {
var accounts = getAccounts();
if (accounts.length === 0) {
throw new Error('NO_ACCOUNTS: Нет сохранённых аккаунтов. Добавьте аккаунт через меню GetCourse → Управление аккаунтами.');
}
var idx = getActiveAccountIndex();
var acc = accounts[idx];
return {
email: acc.email,
password: acc.password,
domain: acc.domain
};
}
// ============================================================
// Диалог управления аккаунтами
// ============================================================
function showAccountManager() {
var html = HtmlService.createHtmlOutput(getAccountManagerHtml_())
.setWidth(520)
.setHeight(480)
.setTitle('Управление аккаунтами');
SpreadsheetApp.getUi().showModalDialog(html, 'Управление аккаунтами GetCourse');
}
function getAccountManagerHtml_() {
return '\
<!DOCTYPE html>\
<html>\
<head>\
<style>\
* { box-sizing: border-box; font-family: "Google Sans", Roboto, Arial, sans-serif; }\
body { margin: 0; padding: 16px; font-size: 14px; color: #202124; }\
h3 { margin: 0 0 12px; font-size: 16px; }\
.account-list { margin-bottom: 16px; }\
.account-item {\
display: flex; align-items: center; padding: 10px 12px;\
border: 1px solid #dadce0; border-radius: 8px; margin-bottom: 8px;\
cursor: pointer; transition: background 0.15s;\
}\
.account-item:hover { background: #f1f3f4; }\
.account-item.active { border-color: #1a73e8; background: #e8f0fe; }\
.account-item .info { flex: 1; margin-left: 10px; }\
.account-item .name { font-weight: 500; }\
.account-item .email { font-size: 12px; color: #5f6368; }\
.account-item .domain { font-size: 11px; color: #80868b; }\
.account-item .actions { display: flex; gap: 4px; }\
.btn { padding: 6px 16px; border-radius: 4px; border: 1px solid #dadce0;\
background: #fff; cursor: pointer; font-size: 13px; }\
.btn:hover { background: #f1f3f4; }\
.btn-primary { background: #1a73e8; color: #fff; border: none; }\
.btn-primary:hover { background: #1557b0; }\
.btn-danger { color: #d93025; border-color: #d93025; }\
.btn-danger:hover { background: #fce8e6; }\
.btn-sm { padding: 4px 10px; font-size: 12px; }\
.form-group { margin-bottom: 12px; }\
.form-group label { display: block; margin-bottom: 4px; font-size: 12px; color: #5f6368; }\
.form-group input { width: 100%; padding: 8px 10px; border: 1px solid #dadce0;\
border-radius: 4px; font-size: 14px; }\
.form-group input:focus { outline: none; border-color: #1a73e8; }\
.form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }\
.radio { width: 16px; height: 16px; accent-color: #1a73e8; cursor: pointer; }\
.empty { text-align: center; padding: 24px; color: #80868b; }\
#form-section { display: none; }\
</style>\
</head>\
<body>\
\
<div id="list-section">\
<h3>Аккаунты</h3>\
<div id="account-list" class="account-list"><div class="empty">Загрузка...</div></div>\
<button class="btn btn-primary" onclick="showForm(-1)">+ Добавить аккаунт</button>\
</div>\
\
<div id="form-section">\
<h3 id="form-title">Добавить аккаунт</h3>\
<input type="hidden" id="edit-index" value="-1">\
<div class="form-group">\
<label>Название (для удобства)</label>\
<input type="text" id="f-name" placeholder="Например: Фитнес Мама">\
</div>\
<div class="form-group">\
<label>Email</label>\
<input type="email" id="f-email" placeholder="user@example.com">\
</div>\
<div class="form-group">\
<label>Пароль</label>\
<input type="password" id="f-password" placeholder="Пароль от GetCourse">\
</div>\
<div class="form-group">\
<label>Домен школы</label>\
<input type="url" id="f-domain" placeholder="https://myschool.getcourse.ru">\
</div>\
<div class="form-actions">\
<button class="btn" onclick="showList()">Отмена</button>\
<button class="btn btn-primary" onclick="saveForm()">Сохранить</button>\
</div>\
</div>\
\
<script>\
var currentData = { accounts: [], activeIndex: 0 };\
\
function load() {\
google.script.run.withSuccessHandler(function(data) {\
currentData = data;\
render();\
}).getAccountsForDialog();\
}\
\
function render() {\
var list = document.getElementById("account-list");\
if (currentData.accounts.length === 0) {\
list.innerHTML = "<div class=\\"empty\\">Нет аккаунтов. Добавьте первый аккаунт.</div>";\
return;\
}\
var html = "";\
for (var i = 0; i < currentData.accounts.length; i++) {\
var a = currentData.accounts[i];\
var isActive = (i === currentData.activeIndex);\
html += "<div class=\\"account-item" + (isActive ? " active" : "") + "\\">";\
html += "<input type=\\"radio\\" class=\\"radio\\" name=\\"active\\" " + (isActive ? "checked" : "") + " onclick=\\"activate(" + i + ")\\">";\
html += "<div class=\\"info\\">";\
html += "<div class=\\"name\\">" + esc(a.name || "Без названия") + (isActive ? " ✓" : "") + "</div>";\
html += "<div class=\\"email\\">" + esc(a.email) + "</div>";\
html += "<div class=\\"domain\\">" + esc(a.domain) + "</div>";\
html += "</div>";\
html += "<div class=\\"actions\\">";\
html += "<button class=\\"btn btn-sm\\" onclick=\\"event.stopPropagation();showForm(" + i + ")\\">Изм.</button>";\
html += "<button class=\\"btn btn-sm btn-danger\\" onclick=\\"event.stopPropagation();del(" + i + ")\\">✕</button>";\
html += "</div></div>";\
}\
list.innerHTML = html;\
}\
\
function esc(s) { var d = document.createElement("div"); d.textContent = s; return d.innerHTML; }\
\
function activate(idx) {\
google.script.run.withSuccessHandler(function() {\
currentData.activeIndex = idx;\
render();\
}).setActiveAccount(idx);\
}\
\
function del(idx) {\
if (!confirm("Удалить аккаунт \\"" + (currentData.accounts[idx].name || currentData.accounts[idx].email) + "\\"?")) return;\
google.script.run.withSuccessHandler(function() { load(); }).deleteAccount(idx);\
}\
\
function showForm(idx) {\
document.getElementById("list-section").style.display = "none";\
document.getElementById("form-section").style.display = "block";\
document.getElementById("edit-index").value = idx;\
if (idx >= 0 && idx < currentData.accounts.length) {\
var a = currentData.accounts[idx];\
document.getElementById("form-title").textContent = "Редактировать аккаунт";\
document.getElementById("f-name").value = a.name || "";\
document.getElementById("f-email").value = a.email || "";\
document.getElementById("f-password").value = a.password || "";\
document.getElementById("f-domain").value = a.domain || "";\
} else {\
document.getElementById("form-title").textContent = "Добавить аккаунт";\
document.getElementById("f-name").value = "";\
document.getElementById("f-email").value = "";\
document.getElementById("f-password").value = "";\
document.getElementById("f-domain").value = "";\
}\
}\
\
function showList() {\
document.getElementById("form-section").style.display = "none";\
document.getElementById("list-section").style.display = "block";\
}\
\
function saveForm() {\
var idx = parseInt(document.getElementById("edit-index").value, 10);\
var data = {\
index: idx >= 0 ? idx : null,\
name: document.getElementById("f-name").value.trim(),\
email: document.getElementById("f-email").value.trim(),\
password: document.getElementById("f-password").value,\
domain: document.getElementById("f-domain").value.trim().replace(/\\/+$/, "")\
};\
if (!data.email || !data.password || !data.domain) {\
alert("Заполните Email, Пароль и Домен.");\
return;\
}\
google.script.run.withSuccessHandler(function() {\
showList();\
load();\
}).saveAccount(data);\
}\
\
load();\
</script>\
</body>\
</html>';
}
// ============================================================
// Авторизация
// ============================================================
/**
* Логин в GetCourse по email/паролю через AJAX API.
* GetCourse использует React-форму и эндпоинт /user/public/user/json
* Возвращает строку cookies для авторизованной сессии.
*/
function login() {
var config = getConfig();
var ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
// Шаг 1: GET любой страницы для получения начального cookie сессии
var getResp = UrlFetchApp.fetch(config.domain + '/cms/system/login', {
method: 'get',
headers: { 'User-Agent': ua },
muteHttpExceptions: true,
followRedirects: true
});
var initialCookies = extractAllCookies_(getResp);
// Шаг 2: AJAX-логин через /user/public/user/json
var ajaxUrl = config.domain + '/user/public/user/json';
var postResp = UrlFetchApp.fetch(ajaxUrl, {
method: 'post',
headers: {
'User-Agent': ua,
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'Referer': config.domain + '/cms/system/login',
'Cookie': initialCookies
},
payload: 'action=login&email=' + encodeURIComponent(config.email) +
'&password=' + encodeURIComponent(config.password),
muteHttpExceptions: true,
followRedirects: false
});
var postCode = postResp.getResponseCode();
var postBody = postResp.getContentText();
var postCookies = extractAllCookies_(postResp);
var finalCookies = mergeCookies_(initialCookies, postCookies);
// Парсим JSON-ответ
var json;
try {
json = JSON.parse(postBody);
} catch (e) {
throw new Error('LOGIN_FAILED: Неожиданный ответ сервера (HTTP ' + postCode + '): ' + postBody.substring(0, 200));
}
// Проверяем результат
if (json.error) {
throw new Error('LOGIN_FAILED: ' + (json.error.message || json.error.text || JSON.stringify(json.error)));
}
// Шаг 3: Если в ответе есть redirectUrl — переходим по нему (устанавливает финальные cookies)
if (json.data && json.data.redirectUrl) {
var redirectUrl = json.data.redirectUrl;
if (redirectUrl.indexOf('http') !== 0) {
redirectUrl = config.domain + redirectUrl;
}
var redirResp = UrlFetchApp.fetch(redirectUrl, {
method: 'get',
headers: { 'User-Agent': ua, 'Cookie': finalCookies },
muteHttpExceptions: true,
followRedirects: true
});
var redirCookies = extractAllCookies_(redirResp);
finalCookies = mergeCookies_(finalCookies, redirCookies);
}
// Шаг 4: Проверяем что авторизация работает
var testResp = UrlFetchApp.fetch(config.domain + '/notifications/control/mailings/active', {
method: 'get',
headers: { 'User-Agent': ua, 'Cookie': finalCookies },
muteHttpExceptions: true,
followRedirects: true
});
var testCookies = extractAllCookies_(testResp);
finalCookies = mergeCookies_(finalCookies, testCookies);
if (isLoginPage_(testResp.getContentText())) {
throw new Error('LOGIN_FAILED: Логин прошёл, но сессия не активна. Ответ JSON: ' + postBody.substring(0, 300));
}
// Сохраняем cookies в кеш на 2 часа
CacheService.getScriptCache().put('gc_session', finalCookies, 7200);
return finalCookies;
}
/**
* Извлекает ВСЕ cookies из заголовков Set-Cookie ответа.
* Возвращает строку "name1=val1; name2=val2"
*/
function extractAllCookies_(response) {
var allHeaders = response.getAllHeaders();
var setCookieHeaders = allHeaders['Set-Cookie'];
if (!setCookieHeaders) return '';
if (typeof setCookieHeaders === 'string') {
setCookieHeaders = [setCookieHeaders];
}
var cookies = {};
for (var i = 0; i < setCookieHeaders.length; i++) {
var pair = setCookieHeaders[i].split(';')[0]; // берём только name=value
var eqIdx = pair.indexOf('=');
if (eqIdx > 0) {
var name = pair.substring(0, eqIdx).trim();
var value = pair.substring(eqIdx + 1).trim();
cookies[name] = value;
}
}
var parts = [];
for (var key in cookies) {
parts.push(key + '=' + cookies[key]);
}
return parts.join('; ');
}
/**
* Объединяет две строки cookies. Новые перезаписывают старые.
*/
function mergeCookies_(existing, fresh) {
if (!fresh) return existing;
if (!existing) return fresh;
var cookies = {};
// Парсим существующие
var parts = existing.split(';');
for (var i = 0; i < parts.length; i++) {
var eqIdx = parts[i].indexOf('=');
if (eqIdx > 0) {
cookies[parts[i].substring(0, eqIdx).trim()] = parts[i].substring(eqIdx + 1).trim();
}
}
// Перезаписываем новыми
parts = fresh.split(';');
for (var j = 0; j < parts.length; j++) {
var eqIdx2 = parts[j].indexOf('=');
if (eqIdx2 > 0) {
cookies[parts[j].substring(0, eqIdx2).trim()] = parts[j].substring(eqIdx2 + 1).trim();
}
}
var result = [];
for (var key in cookies) {
result.push(key + '=' + cookies[key]);
}
return result.join('; ');
}
/**
* Возвращает актуальный cookie сессии.
* Сначала проверяет кеш, если нет — логинится заново.
*/
function getSessionCookie() {
var cached = CacheService.getScriptCache().get('gc_session');
if (cached) return cached;
return login();
}
// ============================================================
// HTTP-запрос к GetCourse
// ============================================================
function fetchMailingPage(mailingId) {
var config = getConfig();
var url = config.domain + '/notifications/control/mailings/update/id/' + mailingId + '/part/main';
var sessionCookie = getSessionCookie();
var html = fetchWithSession_(url, sessionCookie);
// Проверяем, не попали ли на страницу логина
if (isLoginPage_(html)) {
// Сессия истекла — перелогиниваемся
CacheService.getScriptCache().remove('gc_session');
sessionCookie = login();
html = fetchWithSession_(url, sessionCookie);
if (isLoginPage_(html)) {
throw new Error('LOGIN_FAILED: Не удалось авторизоваться');
}
}
return html;
}
function fetchWithSession_(url, sessionCookie) {
var response = UrlFetchApp.fetch(url, {
method: 'get',
headers: {
'Cookie': sessionCookie,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7'
},
muteHttpExceptions: true,
followRedirects: true
});
var code = response.getResponseCode();
if (code !== 200) {
throw new Error('HTTP_ERROR_' + code);
}
return response.getContentText();
}
function isLoginPage_(html) {
return html.indexOf('action="/cms/system/login"') !== -1 ||
html.indexOf('id="loginForm"') !== -1 ||
html.indexOf('LoginForm[email]') !== -1 ||
html.indexOf('LoginForm%5Bemail%5D') !== -1 ||
html.indexOf('Pagina de logare') !== -1 ||
html.indexOf('page-cms_system-login') !== -1 ||
(html.indexOf('login') !== -1 && html.indexOf('Статистика рассылки') === -1 && html.indexOf('gc-user-logined') === -1);
}
// ============================================================
// Парсинг HTML
// ============================================================
function parseMailingStats(html) {
var stats = {
title: '',
total: 0,
delivered: 0,
views: 0,
clicks: 0,
unsubscribes: 0,
errors: 0,
restricted: 0
};
// Название рассылки из <title>
var titleMatch = html.match(/<title>([^<]+)<\/title>/);
if (titleMatch) {
stats.title = titleMatch[1].trim();
}
// --- Панель 1: "Статистика рассылки" (values-table) ---
// Всего сообщений
var totalMatch = html.match(/Всего сообщений<\/td>\s*<td[^>]*>\s*<a[^>]*>\s*([\d\s]+)\s*<\/a>/);
if (totalMatch) {
stats.total = parseInt(totalMatch[1].replace(/\s/g, ''), 10) || 0;
}
// доставлено
var deliveredMatch = html.match(/<td class="subkey">\s*доставлено\s*<\/td>\s*<td[^>]*>\s*<a[^>]*>\s*([\d\s]+)\s*<\/a>/);
if (deliveredMatch) {
stats.delivered = parseInt(deliveredMatch[1].replace(/\s/g, ''), 10) || 0;
}
// ошибка
var errorsMatch = html.match(/<td class="subkey">\s*ошибка\s*<\/td>\s*<td[^>]*>\s*<a[^>]*>\s*([\d\s]+)\s*<\/a>/);
if (errorsMatch) {
stats.errors = parseInt(errorsMatch[1].replace(/\s/g, ''), 10) || 0;
}
// запрещено
var restrictedMatch = html.match(/<td class="subkey">\s*запрещено\s*<\/td>\s*<td[^>]*>\s*<a[^>]*>\s*([\d\s]+)\s*<\/a>/);
if (restrictedMatch) {
stats.restricted = parseInt(restrictedMatch[1].replace(/\s/g, ''), 10) || 0;
}
// --- Панель 2: "Статистика пользователей" ---
// просмотров (открытия)
var viewsMatch = html.match(/([\d\s]+)\s*просмотр[а-я]*\s*\(/);
if (viewsMatch) {
stats.views = parseInt(viewsMatch[1].replace(/\s/g, ''), 10) || 0;
}
// кликов
var clicksMatch = html.match(/([\d\s]+)\s*клик[а-я]*\s*\(/);
if (clicksMatch) {
stats.clicks = parseInt(clicksMatch[1].replace(/\s/g, ''), 10) || 0;
}
// отписки
var unsubMatch = html.match(/([\d\s]+)\s*отписк[а-яё]*\s*\(/);
if (unsubMatch) {
stats.unsubscribes = parseInt(unsubMatch[1].replace(/\s/g, ''), 10) || 0;
}
return stats;
}
// ============================================================
// Извлечение ID из ячейки
// ============================================================
function extractMailingId_(cellValue) {
if (!cellValue) return null;
var str = String(cellValue).split(/\s*\|\s*/)[0].trim();
// Если чистое число
if (/^\d+$/.test(str)) return str;
// URL с /id/ЧИСЛО
var urlMatch = str.match(/\/id\/(\d+)/);
if (urlMatch) return urlMatch[1];
// Первое число в строке
var numMatch = str.match(/(\d{5,})/);
if (numMatch) return numMatch[1];
return null;
}
// ============================================================
// Обновление статистики
// ============================================================
function updateSelectedRows() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var selection = sheet.getActiveRange();
if (!selection) {
SpreadsheetApp.getUi().alert('Выделите ячейки с ID рассылок.');
return;
}
var updated = 0;
var numRows = selection.getNumRows();
var numCols = selection.getNumColumns();
var startRow = selection.getRow();
var startCol = selection.getColumn();
for (var r = 0; r < numRows; r++) {
for (var c = 0; c < numCols; c++) {
var cell = sheet.getRange(startRow + r, startCol + c);
var cellValue = cell.getValue();
if (!cellValue) continue;
var mailingId = extractMailingId_(cellValue);
if (!mailingId) continue;
ss.toast('Обработка ID ' + mailingId + '...', 'GetCourse', 3);
var row = startRow + r;
var col = startCol + c;
try {
var html = fetchMailingPage(mailingId);
var stats = parseMailingStats(html);
var now = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'dd.MM.yyyy HH:mm');
var fields = [
[stats.title, 'Название'],
[stats.total, 'Всего'],
[stats.delivered, 'Доставлено'],
[stats.views, 'Просмотры'],
[stats.clicks, 'Клики'],
[stats.unsubscribes, 'Отписки'],
[stats.errors, 'Ошибки'],
[stats.restricted, 'Запрещено'],
[now, 'Обновлено']
];
for (var i = 0; i < fields.length; i++) {
sheet.getRange(row, col + 1 + i).setValue(fields[i][0]).setNote(fields[i][1]);
}
SpreadsheetApp.flush();
updated++;
} catch (e) {
if (e.message.indexOf('LOGIN_FAILED') !== -1) {
SpreadsheetApp.getUi().alert('Ошибка авторизации: ' + e.message);
return;
}
sheet.getRange(row, col + 1).setValue('Ошибка: ' + e.message).setNote('Ошибка');
SpreadsheetApp.flush();
}
Utilities.sleep(500);
}
}
ss.toast('Обновлено: ' + updated, 'GC: Email Stats', 5);
}
// ============================================================
// Сегменты: количество пользователей
// ============================================================
function extractSegmentUrl_(cell) {
// HYPERLINK-формула: =HYPERLINK("url", "text")
var formula = cell.getFormula();
if (formula) {
var m = formula.match(/HYPERLINK\s*\(\s*"([^"]+)"/i);
if (m) return m[1];
}
// Rich-text ссылка
var richText = cell.getRichTextValue();
if (richText) {
var url = richText.getLinkUrl();
if (url) return url;
}
// Обычный текст URL
var val = String(cell.getValue()).trim();
if (val.match(/^https?:\/\//)) return val;
return null;
}
function fetchSegmentPage(url) {
var sessionCookie = getSessionCookie();
var html = fetchWithSession_(url, sessionCookie);
if (isLoginPage_(html)) {
CacheService.getScriptCache().remove('gc_session');
sessionCookie = login();
html = fetchWithSession_(url, sessionCookie);
if (isLoginPage_(html)) {
throw new Error('LOGIN_FAILED: Не удалось авторизоваться');
}
}
return html;
}
function parseSegmentCount(html) {
// <div class="summary">Показаны <b>1-30</b> из <b>1 941</b> записи.</div>
var m = html.match(/<div class="summary">Показаны\s*<b>[^<]+<\/b>\s*из\s*<b>([\d\s]+)<\/b>/);
if (m) return parseInt(m[1].replace(/\s/g, ''), 10);
return null;
}
function updateSegmentCounts() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var selection = sheet.getActiveRange();
if (!selection) {
SpreadsheetApp.getUi().alert('Выделите ячейки со ссылками на сегменты.');
return;
}
var updated = 0;
var numRows = selection.getNumRows();
var numCols = selection.getNumColumns();
var startRow = selection.getRow();
var startCol = selection.getColumn();
for (var r = 0; r < numRows; r++) {
for (var c = 0; c < numCols; c++) {
var cell = sheet.getRange(startRow + r, startCol + c);
if (!cell.getValue()) continue;
var url = extractSegmentUrl_(cell);
if (!url) continue;
ss.toast('Обработка сегмента...', 'GC: Сегменты', 3);
try {
var html = fetchSegmentPage(url);
var count = parseSegmentCount(html);
if (count === null) {
ss.toast('Не удалось найти количество пользователей', 'GC: Сегменты', 3);
continue;
}
var now = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'dd.MM.yyyy HH:mm');
var col = startCol + c;
sheet.getRange(startRow + r, col + 1).setValue(count);
sheet.getRange(startRow + r, col + 2).setValue(now);
SpreadsheetApp.flush();
updated++;
} catch (e) {
if (e.message.indexOf('LOGIN_FAILED') !== -1) {
SpreadsheetApp.getUi().alert('Ошибка авторизации: ' + e.message);
return;
}
sheet.getRange(startRow + r, startCol + c + 1).setValue('Ошибка: ' + e.message);
SpreadsheetApp.flush();
}
Utilities.sleep(500);
}
}
ss.toast('Обновлено: ' + updated, 'GC: Сегменты', 5);
}
// ============================================================
// Тест подключения
// ============================================================
function testConnection() {
var ui = SpreadsheetApp.getUi();
var config = getConfig();
try {
CacheService.getScriptCache().remove('gc_session');
var sessionCookie = login();
ui.alert('Подключение успешно!',
'Email: ' + config.email + '\n' +
'Домен: ' + config.domain + '\n' +
'Cookie: ' + sessionCookie.substring(0, 40) + '...\n\n' +
'Авторизация работает. Можно обновлять статистику.',
ui.ButtonSet.OK);
} catch (e) {
ui.alert('Ошибка подключения', e.message, ui.ButtonSet.OK);
}
}
/**
* Отладка сегмента: показывает что возвращает сервер для URL сегмента.
* Замените testUrl на нужную ссылку, запустите из Apps Script.
*/
function debugSegment() {
var testUrl = 'https://fitnessmama.school/pl/user/user/index?uc%5Bsegment_id%5D=0&uc%5Brule_string%5D=%7B%22type%22%3A%22user_createdat%22%2C%22inverted%22%3A0%2C%22params%22%3A%7B%22value%22%3A%7B%22from%22%3Anull%2C%22to%22%3Anull%2C%22toNDays%22%3A%2230%22%2C%22fromNDays%22%3Anull%2C%22dateType%22%3A%22last_n_days%22%2C%22withTime%22%3Afalse%7D%2C%22valueMode%22%3Anull%7D%2C%22maxSize%22%3A%22%22%7D&formParams%5Bclarity_uid%5D=JoSTNr9IVThIbcbL_OLoVFNrnDiOXnbX';
try {
var html = fetchSegmentPage(testUrl);
var title = html.match(/<title>([^<]+)<\/title>/);
var isLogin = isLoginPage_(html);
var hasFilterCount = html.indexOf('filter-count') !== -1;
var hasDataCount = html.indexOf('data-count') !== -1;
var count = parseSegmentCount(html);
Logger.log('=== DEBUG segment ===');
Logger.log('URL: ' + testUrl.substring(0, 80) + '...');
Logger.log('Title: ' + (title ? title[1] : 'не найден'));
Logger.log('isLoginPage: ' + isLogin);
Logger.log('hasFilterCount: ' + hasFilterCount);
Logger.log('hasDataCount: ' + hasDataCount);
Logger.log('parsedCount: ' + count);
Logger.log('HTML length: ' + html.length);
Logger.log('First 2000 chars: ' + html.substring(0, 2000));
} catch (e) {
Logger.log('ERROR: ' + e.message);
}
}
/**
* Отладка: показывает что возвращает сервер для конкретного mailing ID.
* Запустите вручную из Apps Script, изменив ID ниже.
*/
function debugMailing() {
var testId = 4508232; // Замените на нужный ID
CacheService.getScriptCache().remove('gc_session');
try {
var html = fetchMailingPage(testId);
var title = html.match(/<title>([^<]+)<\/title>/);
var hasStats = html.indexOf('Статистика рассылки') !== -1;
var hasUserStats = html.indexOf('просмотр') !== -1;
var isLogin = isLoginPage_(html);
var bodyClass = html.match(/class="([^"]*page-[^"]*)"/) || ['', 'не найден'];
Logger.log('=== DEBUG mailing ID: ' + testId + ' ===');
Logger.log('Title: ' + (title ? title[1] : 'не найден'));
Logger.log('Body class: ' + bodyClass[1]);
Logger.log('isLoginPage: ' + isLogin);
Logger.log('hasStats: ' + hasStats);
Logger.log('hasUserStats: ' + hasUserStats);
Logger.log('HTML length: ' + html.length);
Logger.log('First 1000 chars: ' + html.substring(0, 1000));
} catch (e) {
Logger.log('ERROR: ' + e.message);
}
}