Телеграм-бот хакерспейса на TypeScript + mtcute: сборы донатов, присутствие резидентов, статус 3D-принтера, ивенты и миниапп хостинга гостей. Стейт — один JSON-файл.
Работает только в чатах из ALLOWED_CHATS, в остальных молчит. Резиденты — админы этих чатов.
Один сбор на период, по умолчанию — месяц UTC.
/goals— показать текущий сбор/donate <сумма> <ник>— добавить донат; без ника — анонимный/remove <номер>— удалить донаты участника по месту в лидерборде/remove <ник> [сумма]— удалить один донат/setgoal <сумма>— цель,0снимает/settitle <тема>— тема сбора, по умолчанию «аренду»/setdesc <текст>— описание под сбором (реквизиты, многострочное); без текста убирает/setresetday <1–29>— день сброса, по умолчанию 1/export [all]— выгрузка донатов в CSV: текущий сбор или все периоды/goalsmute— вкл/выкл ежедневный постинг в этот чат
Смена дня сброса меняет и формулу ключа периода, поэтому /setresetday в середине месяца переводит текущий сбор в /history и начинает новый с теми же настройками — бот об этом предупреждает в ответе.
Лидерборд группируется по нику и сортируется по сумме, топ-3 — 🥇🥈🥉, при достижении цели — «✅ Сбор закрыт». Сообщение перерисовывается на месте при правках, при смене периода переключается на новый сбор. Каждый день в 12:00 МСК постится свежее сообщение. Всё, кроме /goals и /help, — только для админов.
/startв личке — меню «Отметиться с ником / без ника»/inside— показать разовый список тех, кто в спейсе; в группе доступна всем участникам, в личке — только им же (посторонним бот его не отдаёт)/bindmac <MAC>,/unbindmac,/maclist,/settings— авто-отметки по MAC
Кто сейчас внутри показывается на закреплённой доске «кто сегодня в спейсе» (блок «Сейчас в спейсе»); чек-ин/чек-аут её обновляют. Отдельной авто-рассылки списка больше нет — выключить доску в чате можно через /boardmute. Каждые 3 часа бот пингует в личку: «Я внутри» продлевает, 15 минут без ответа — отметка снимается.
Авто-отметки по MAC: бот опрашивает роутер Keenetic и отмечает резидента, пока устройство в сети, снимает через 10 минут отсутствия. Такие отметки не пингуются. На устройстве нужно выключить рандомизацию MAC для Wi-Fi спейса.
/printer — статус из Moonraker (Klipper): в allowlist-чатах доступен участникам, в личке — только резидентам (за кнопкой «Камера» живой кадр из помещения). При активной печати показывает прогресс, превью (миниатюра gcode / снимок вебки, переключаются кнопками) и кнопку «Уведомить по окончании» — подписчик получит сообщение в личку, когда печать завершится.
Ивент — то, ради чего в спейс приходят вместе: воркшоп, ремонт-кафе, демо-день. Заводит резидент, живёт в том же окне ближайших 7 дней, что и хостинг.
- Экран дня в миниаппе: список ивентов и «Создать ивент» — название, описание, день, время и тумблер «только резидентам».
- Ивент виден в «Активности» у гостей и на закреплённой доске дня; помеченный «только резидентам» — не виден нигде, кроме резидентских экранов.
- Резидент может переслать боту в личку пост из канала анонсов (
ANNOUNCE_CHANNEL_ID) — бот подтянет текст и картинку и предложит открыть редактор с уже заполненными полями. Форматирование поста при этом теряется: редактор — обычное текстовое поле.
Афиши лежат файлами рядом со стейтом (event-photos/), а не в JSON, и отдаются по подписанной ссылке /event-photo.jpg.
Гость оставляет заявку на визит, резидент её одобряет.
- Гость: день из ближайших 7, время и цель. Видит только свои визиты; когда заявку одобрят — бот напишет, кто хостит.
- Резидент: обзор на 7 дней со всеми заявками, детали дня, кнопка «Захостить». Одобривший может снять своё одобрение. Архив прошедших недель — только просмотр.
- Уведомления о новых заявках приходят в личку, по умолчанию только о заявках на сегодня; в настройках — все либо выкл.
- Настройки авто-отметки по MAC продублированы в миниаппе, стейт общий с
/bindmacи/settings. - Тема системная, в настройках можно закрепить светлую или тёмную. Гостю в настройках доступна только тема.
- В подтверждённом визите есть «Добавить в календарь» —
.icsпо подписанной ссылке. - Резидент может закрыть заявку, которую спейс не потянет («Закрыть заявку» в свайпе по строке): заявка удаляется, гостю уходит DM с предложением выбрать другой день. Подтверждённый визит закрывает только его хост.
- Экран «Как пройти» у гостя: адрес с копированием, ближайшее метро, фото двери подъезда и точка на карте.
- Для
DEV_USER_IDSесть дев-меню: перспектива «резидент ↔ гость», фейковые заявки, правка и удаление любых заявок. Фейковые заявки на публичные поверхности (доска, «кто придёт») не попадают.
Кнопки входа: рядом с полем ввода в личке, под списком /inside и в /start.
Включается WEBAPP_URL — публичный HTTPS-адрес, под которым отдаётся собранный миниапп из webapp/dist. Бот поднимает HTTP-сервер на WEBAPP_HOST:WEBAPP_PORT, наружу его выставляет реверс-прокси с TLS. Чтобы кнопки из групп открывали миниапп, включи Main Mini App в BotFather (Bot Settings → Configure Mini App) с тем же URL. Авторизация запросов — подпись initData.
cp .env.example .env # API_ID, API_HASH, BOT_TOKEN, ALLOWED_CHATS
npm install
npm --prefix webapp install # зависимости миниаппа (React + Vite)
npm --prefix webapp run build # сборка миниаппа в webapp/dist (если задан WEBAPP_URL)
npm start # npm run dev — watch-режимМиниапп хостинга — отдельное React + Vite + TypeScript приложение в webapp/; сервер бота раздаёт готовую сборку из webapp/dist. Для разработки фронта — npm --prefix webapp run dev (Vite dev-сервер с прокси /api, /avatar.jpg, /visit.ics на бэкенд). В Docker сборка миниаппа — шаг в Dockerfile.
Остальные переменные — в .env.example, каждая выключает свою подсистему, если не задана: PRINTER_URL/PRINTER_AUTH, KEENETIC_URL/KEENETIC_LOGIN/KEENETIC_PASSWORD, WEBAPP_URL/WEBAPP_HOST/WEBAPP_PORT/HOSTING_TZ_OFFSET_MINUTES, ANNOUNCE_CHANNEL_ID, DEV_USER_IDS, DATA_FILE/SESSION_FILE, форвардинг и live-чат.
Стейт — в DATA_FILE (по умолчанию ./data.json), сессия mtcute — в SESSION_FILE (по умолчанию ./bot.session). В Docker оба пути обязаны лежать в примонтированном томе — docker-compose.yaml задаёт их через environment, иначе пересборка образа стирает и стейт, и сессию. Рядом со стейтом бот держит <DATA_FILE>.bak и афиши ивентов; битый data.json не роняет бота, а отводится в .corrupt-<время> с сообщением дев-аккаунтам.
Тестов и линтера нет, проверка — npm run typecheck (бэкенд) и npm --prefix webapp run typecheck (миниапп); оба гоняются в CI.
src/index.ts — точка входа: env, клиент, диспетчер, хендлеры, шедулеры, выключение
src/handlers.ts — команды сборов, пагинация, проверки allowlist/админа
src/menu.ts — /start и /menu в личке: меню резидента (присутствие, MAC, принтер, миниапп)
src/fundraiser.ts — модель сбора: лидерборд, прогресс-бар, периоды, парсинг
src/presence.ts — присутствие: чек-ин/чек-аут, пинги, поллер MAC
src/residents.ts — ResidentDirectory: кто резидент, участник и админ
src/keenetic.ts — клиент RCI-API Keenetic для авто-отметок по MAC
src/printer.ts — /printer, Moonraker, поллер окончания печати
src/hosting.ts — модель хостинга: заявки, дни/недели, архив, переносы, заметки, .ics
src/hosting-board.ts— закреплённая доска «кто сегодня в спейсе»
src/hosting-invite.ts— «позвать в спейс»: кандидаты на день и зов в личку
src/events.ts — модель ивентов и файлы афиш
src/event-intake.ts — пересланный пост канала → заготовка ивента
src/announce.ts — анонсы: чтение релизов GitHub и рассылка по чатам
src/backup.ts — дев-бэкапы стейта: /backup и /autobackup
src/webapp.ts — HTTP-сервер миниаппа: статика webapp/dist + JSON API + картинки
src/scheduler.ts — смена периода + ежедневный постинг сбора
src/storage.ts — JSON-хранилище: атомарная запись, резервная копия, восстановление
src/forwarder.ts — форвардинг постов из канала в чат (FORWARD_FROM_CHAT/FORWARD_TO_CHAT)
src/livechat.ts — гард «живого» чата: кикает входящих (LIVE_CHAT_ID)
src/errors.ts — отчёты об ошибках в личку дев-аккаунтам
src/types.ts — типы стейта
webapp/ — фронт миниаппа (React + Vite + TypeScript, iOS, светлая и тёмная темы)