Перейти к содержанию

Сколько часов я отработал в Битрикс24: расширение для браузера без админа и вебхуков

Я работаю над проектом, где задачи ведутся в Битрикс24. Условие простое: не меньше 20 часов в месяц. Больше можно, если есть задачи, меньше нельзя. Поэтому каждую задачу я засекаю штатным таймером, а в конце месяца сверяю, сколько набежало. Сам Битрикс24 по месяцам это не считает: в списке задач колонка «Затрачено» показывает время за всю жизнь задачи. Сумму выдавало приложение из Маркета.

25 октября приложение сломалось. Окно открывалось, настройки сохранялись, а сервер разработчика на каждый запрос отчёта отвечал кодом 500.

Ошибка 500 от сервера приложения в консоли браузера

Починить это со своей стороны нельзя никак. Ночью я понял, что могу попросить ИИ-агента посчитать часы без чужих приложений, и к утру у меня было своё расширение для браузера: одна кнопка, таблица по задачам и итог за месяц. Ниже о том, как мы к нему пришли через два тупика и почему главная часть решения — один запрос fetch.

Кому пригодится: сотрудникам на облачном портале Битрикс24 без прав администратора и без подписки на Маркет, которым нужно знать своё время по задачам за месяц. Работает в Chrome, Яндекс Браузере и других браузерах на Chromium. Код открыт: git.08h.ru/alex/bitrix24-my-time.

Окно расширения: время по задачам за октябрь и итог 10:54, названия задач размыты

Тупик №1: своё приложение для портала

Первая идея, которую предложил агент, была очевидной: написать локальное приложение для Битрикс24. Это страница, которая встраивается в портал, получает доступ к REST API и может жить хоть в левом меню. Агент даже собрал её: один index.html в zip-архиве, который портал хранит сам, без всякого хостинга.

Поставить её я не смог. В разделе «Разработчикам» → «Другое» у меня было только два пункта, исходящий и входящий вебхук. Локальные приложения добавляет администратор портала, а я на этом портале обычный сотрудник.

Тупик №2: входящий вебхук

Вебхук — это адрес с секретным ключом, через который скрипт работает с порталом от вашего имени. Создать его может и сотрудник, так что агент переписал подсчёт на Python: скрипт берёт адрес вебхука из файла и печатает часы в консоль.

Я открыл «Входящий вебхук» и увидел рекламу и предложение купить подписку. На портале нет подписки на Маркет, а без неё вебхуки закрыты. Тупик.

Находка: портал сам ходит в REST от вашего имени

Тот, кто вошёл в Битрикс24 через браузер, уже авторизован. Страницы портала держат идентификатор сессии BX.bitrix_sessid(), и с ним REST API принимает запросы без всяких вебхуков. Проверить это можно за минуту: открыть портал, нажать F12, вкладка Console, и вставить строку:

fetch('/rest/task.elapseditem.getlist.json?sessid=' + BX.bitrix_sessid(), {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ORDER: {ID: 'desc'}, FILTER: {}, SELECT: ['ID', 'TASK_ID', 'SECONDS', 'CREATED_DATE'], PARAMS: {NAV_PARAMS: {nPageSize: 5}}})}).then(r => r.json()).then(console.log)

Ответ пришёл: пять последних записей затраченного времени с номерами задач, секундами и датами. Значит, всё, что нужно, доступно с правами обычного сотрудника. Запрос только читает данные и ничего не меняет.

Грабли №1: «allow pasting». Агент предупредил, что Chrome может запретить вставку в консоль и попросить набрать allow pasting. Я набрал эти слова и всякий раз получал Uncaught SyntaxError: Unexpected identifier 'pasting': консоль выполнила фразу как код на JavaScript. А вот когда я вставил код, который выше, консоль потребовала ввести allow pasting, что я и сделал. После этого снова вставил код, и тогда он сработал. Это не ошибка браузера или портала, это такая защита.
Грабли №2: чужое время. В первом ответе было поле "total": 6683. Столько записей у меня быть не могло: метод отдаёт время всех людей по всем задачам, которые вам видны. Поэтому фильтр по пользователю обязателен, а свой ID расширение берёт из user.current.

Как считается месяц

Записи о затраченном времени отдаёт метод task.elapseditem.getlist. Фильтр — мой ID и даты месяца, страницы по 50 записей:

for (let page = 1; ; page++) {
  const items = await call('task.elapseditem.getlist', {
    ORDER: { ID: 'asc' },
    FILTER: { USER_ID: me, '>=CREATED_DATE': ym + '-01T00:00:00', '<CREATED_DATE': next + '-01T00:00:00' },
    SELECT: ['ID', 'TASK_ID', 'SECONDS', 'CREATED_DATE'],
    PARAMS: { NAV_PARAMS: { nPageSize: 50, iNumPage: page } },
  });
  for (const it of items) if (it.CREATED_DATE.startsWith(ym)) perTask[it.TASK_ID] = (perTask[it.TASK_ID] || 0) + Number(it.SECONDS);
  if (items.length < 50) break;
}

Проверка startsWith(ym) дублирует фильтр по дате. Старые методы задач понимают формат дат по-разному, и если портал проигнорирует условие, в сумму всё равно не попадёт чужой месяц. Названия задач добираются потом одним запросом tasks.task.list на каждые 50 задач.

Сама функция call — это тот же fetch, что и в консоли, только с проверкой ошибки:

const call = async (method, params) => {
  const r = await fetch('/rest/' + method + '.json?sessid=' + BX.bitrix_sessid(), {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params),
  });
  const d = await r.json();
  if (d.error) throw d.error + ' ' + (d.error_description || '');
  return d.result;
};
Грабли №3: таймер должен быть остановлен. Запись о затраченном времени появляется, когда таймер останавливают. Пока он тикает в задаче, этих минут в отчёте нет. Так что для точных цифр следует нажать кнопку «Пауза» в таймере.

Портал не торопится: в моём тесте один запрос шёл 2,15 секунды. Месяц с десятками записей считается за несколько секунд.

Из закладки в расширение

Сначала это была закладка: javascript:-ссылка на панели браузера, которая рисует окно поверх портала. Но мне хотелось нормальную кнопку в отдельном профиле браузера, где у меня открыт рабочий портал, поэтому я попросил сделать расширение. Оно получилось совсем маленьким: манифест, фоновый скрипт на 16 строк, страница настроек и тот же код подсчёта на 76 строк.

Главная тонкость — где выполнять код. Обычный скрипт расширения работает в изолированном мире и не видит BX со страницы. Поэтому он внедряется в мир самой страницы, world: 'MAIN':

chrome.action.onClicked.addListener(async tab => {
  const cfg = await chrome.storage.sync.get({ portal: '', norm: 0 });
  if (cfg.portal && !(tab.url || '').startsWith(cfg.portal)) return chrome.tabs.create({ url: cfg.portal });
  const target = { tabId: tab.id };
  try {
    await chrome.scripting.executeScript({ target, world: 'MAIN', func: c => { window.myTimeConfig = c; }, args: [cfg] });
    await chrome.scripting.executeScript({ target, world: 'MAIN', files: ['mytime.js'] });
  } catch {
    chrome.runtime.openOptionsPage();
  }
});

Права расширению нужны минимальные: activeTab, то есть только та вкладка, на которой вы нажали кнопку, scripting и storage для настроек. Доступа к другим сайтам у него нет.

В настройках два поля: адрес портала и норма часов в месяц. Если адрес задан, а вы нажали кнопку на другой вкладке, расширение откроет портал. Если задана норма, под итогом появляется строка «До нормы 20 ч осталось 9:06». Ради этой строки всё и затевалось.

Как поставить у себя

  • Скачайте репозиторий и распакуйте.
  • Откройте chrome://extensions/ (в Яндекс Браузере browser://extensions/) и включите «Режим разработчика».
  • Нажмите «Загрузить распакованное расширение» и выберите папку extension.
  • Откроются настройки: впишите адрес своего портала и норму часов, если она у вас есть.
  • Откройте портал и нажмите значок расширения. Повторное нажатие закрывает окно.

Страница расширений Яндекс Браузера: включён режим разработчика, загружено «Моё время в Битрикс24»

Удаляется расширение кнопкой «Удалить» на той же странице. Если правите код под себя, после правки нажмите ↻ на карточке расширения. Самопроверка подсчёта без портала: node test/test_mytime.mjs должен напечатать ok.

Грабли №4: портал работодателя. Расширение работает строго с вашими правами и только читает ваши же записи времени, но всё-таки это портал компании. Если в ней есть правила насчёт сторонних расширений и интеграций, сначала спросите.

Итог

Было: отчёт о моих часах зависел от чужого сервера, и в ночь, когда этот сервер упал, я остался считать на пальцах. Стало: кнопка в браузере, которая берёт данные прямо из портала. Без администратора, без вебхуков, без подписки и без посредников, полторы сотни строк вместе с настройками.

Про работу с агентом скажу честно: два первых его решения были правильными, но не для моей ситуации. Локальное приложение и вебхук — это как раз то, что советует документация. Сработало только третье, когда мы перестали искать официальный вход и проверили одной строкой в консоли, что портал и так нас пускает. Урок шире Битрикса: прежде чем строить интеграцию, посмотрите, как с сервером разговаривает его собственная страница. Часто нужный ключ у вас уже в кармане.

И второй урок, из дневника: почему я не сделал этого раньше? Потому что приложение работало и меня устраивало. Пока не отвалилось.

Код: git.08h.ru/alex/bitrix24-my-time. Напрашивается продолжение: показывать прогресс к норме прямо на значке расширения и напоминать, если таймер забыт включённым. Если у вас не заводится или портал отвечает ошибкой, пишите в комментариях, разберёмся.

Как это было той ночью, коротко записано в личном дневнике.

Написать комментарий

Адрес почты не публикуется. Обязательные поля отмечены звёздочкой.

Возможно, вы не читали

Как запретить протокол http для сайта?

17 ноября 2016
Разгар дня. Пальцы набивают код, глаза напряженно всматриваются в монитор, неподалеку гудит принтер, выплевывая бумагу, цокают каблучки…