✨ Реорганизация структуры заметок v2.0
- Создана новая организационная структура с эмодзи-папками - Добавлена система Inbox для быстрого захвата идей - Созданы шаблоны для всех типов заметок с YAML метаданными - Перенесен весь контент из старой структуры в новую - Добавлен главный дашборд с динамическими запросами - Создано подробное руководство по использованию системы - Техническая документация реорганизована по типам Основные улучшения: ✅ Inbox-first подход для новых заметок ✅ Тематическая организация по 8 областям знаний ✅ Шаблоны с метаданными для структурированности ✅ Система связей между заметками ✅ Динамические дашборды с аналитикой ✅ Централизованная техническая документация без дублирования
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
---
|
||||
tags:
|
||||
- mvp
|
||||
- telegram
|
||||
- obsidian
|
||||
- go
|
||||
---
|
||||
## 1. Цели MVP
|
||||
|
||||
- Принимать текстовые сообщения в Telegram
|
||||
- Создавать заметки в Obsidian Vault в формате Markdown
|
||||
- Асинхронно синхронизировать изменения с Git-репозиторием
|
||||
- Поддерживать базовое форматирование текста (без LLM)
|
||||
- Поддержка whitelist пользователей
|
||||
|
||||
> **Не включаем на первом этапе:**
|
||||
> Голосовые сообщения, векторный поиск, Quartz-публикация, LLM интеграция
|
||||
|
||||
---
|
||||
|
||||
## 2. Компоненты MVP
|
||||
|
||||
### 2.1 Bot Handler (Go + Telegram API)
|
||||
|
||||
- Используем `telegram-bot-api/v5`
|
||||
- Поддерживаем команды:
|
||||
- `/new <текст>` — создать новую заметку
|
||||
- `/append <частичное_имя>` — дописать к существующей заметке (fuzzy search)
|
||||
- `/list` — список последних 5 заметок
|
||||
- Whitelist авторизация по Telegram User ID
|
||||
- Graceful shutdown с завершением pending операций
|
||||
|
||||
### 2.2 Note Service
|
||||
|
||||
- Создание Markdown-файлов в директории `vault/notes`
|
||||
- Умная генерация имени файла: `YYYY-MM-DD-HHMM-slug.md`
|
||||
- Добавление timestamps в YAML frontmatter
|
||||
- Базовое форматирование (первая строка = заголовок)
|
||||
- Fuzzy search для поиска существующих заметок
|
||||
|
||||
### 2.3 Git Service (Асинхронный)
|
||||
|
||||
- Используем `go-git/v5`
|
||||
- **Eventual consistency pattern:**
|
||||
1. Файл сохраняется мгновенно
|
||||
2. Пользователь получает подтверждение
|
||||
3. Git sync идет в фоне через канал
|
||||
4. Батчевые коммиты каждые 30 секунд
|
||||
- Retry logic при ошибках сети
|
||||
- Graceful handling git failures (не влияют на UX)
|
||||
|
||||
### 2.4 File Service
|
||||
|
||||
- Создает файлы в `vault/notes`
|
||||
- Atomic file operations
|
||||
- Проверка уникальности имени файла
|
||||
- Slug generation для читаемых имен
|
||||
|
||||
---
|
||||
|
||||
## 3. Поток обработки сообщений
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[User] --> B[Telegram Bot]
|
||||
B --> C[Auth Check]
|
||||
C --> D["/new text"]
|
||||
D --> E[Note Service]
|
||||
E --> F[File Service: Save]
|
||||
F --> G["✅ Instant Response"]
|
||||
G --> A
|
||||
|
||||
F --> H[Git Queue]
|
||||
H --> I[Background Worker]
|
||||
I --> J[Batch Commit + Push]
|
||||
|
||||
subgraph "Synchronous (Fast)"
|
||||
C
|
||||
D
|
||||
E
|
||||
F
|
||||
G
|
||||
end
|
||||
|
||||
subgraph "Asynchronous (Eventual)"
|
||||
H
|
||||
I
|
||||
J
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Технологический стек
|
||||
|
||||
**Основные зависимости:**
|
||||
|
||||
- `telegram-bot-api/v5` - Telegram Bot API
|
||||
- `go-git/v5` - Git операции
|
||||
- `viper` - конфигурация
|
||||
- `logrus` - структурированное логирование
|
||||
|
||||
**Конфигурация:**
|
||||
|
||||
```yaml
|
||||
telegram_token: "your_bot_token"
|
||||
vault_path: "./vault/notes"
|
||||
allowed_user_ids: [123456789]
|
||||
|
||||
git:
|
||||
sync_interval: "30s"
|
||||
max_queue_size: 100
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
```
|
||||
|
||||
**Внешние сервисы:**
|
||||
|
||||
- Telegram Bot API
|
||||
- Git remote repository
|
||||
|
||||
---
|
||||
|
||||
## 5. Структура проекта
|
||||
|
||||
```
|
||||
obsidian-telegram-bot/
|
||||
├── cmd/
|
||||
│ └── bot/
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── bot/ # Telegram handlers + auth
|
||||
│ ├── note/ # Note operations + fuzzy search
|
||||
│ ├── git/ # Async git sync
|
||||
│ └── config/ # Configuration management
|
||||
├── configs/
|
||||
│ └── config.yaml
|
||||
├── vault/
|
||||
│ └── notes/ # Generated markdown files
|
||||
├── go.mod
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Архитектурные решения
|
||||
|
||||
### 6.1 Filename Generation
|
||||
|
||||
```go
|
||||
// Пример: "2024-01-15-1430-team-meeting.md"
|
||||
func generateFilename(text string) string {
|
||||
timestamp := time.Now().Format("2006-01-02-1504")
|
||||
slug := slugify(extractTitle(text)) // Первые 3 слова
|
||||
return fmt.Sprintf("%s-%s.md", timestamp, slug)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Error Handling Strategy
|
||||
|
||||
- **File operations:** Fail fast, немедленный ответ пользователю
|
||||
- **Git operations:** Fail gracefully, логирование, retry в фоне
|
||||
- **Network issues:** Не блокируют создание заметок
|
||||
|
||||
### 6.3 Fuzzy Search для /append
|
||||
|
||||
```go
|
||||
// /append meeting -> найдет "2024-01-15-1430-team-meeting.md"
|
||||
func FindNoteByPartialName(query string) ([]string, error)
|
||||
```
|
||||
|
||||
### 6.4 Security
|
||||
|
||||
- Whitelist Telegram User IDs в конфиге
|
||||
- Валидация всех входящих команд
|
||||
- Rate limiting через git sync intervals
|
||||
|
||||
---
|
||||
|
||||
## 7. MVP Success Criteria
|
||||
|
||||
**Функциональные:**
|
||||
|
||||
- ✅ Создание заметки за < 2 секунды
|
||||
- ✅ Git sync работает в фоне без блокировок
|
||||
- ✅ Fuzzy search находит заметки по частичному имени
|
||||
- ✅ Только авторизованные пользователи имеют доступ
|
||||
|
||||
**Технические:**
|
||||
|
||||
- ✅ Graceful shutdown без потери данных
|
||||
- ✅ Логирование всех операций
|
||||
- ✅ Retry logic для git операций
|
||||
- ✅ Читаемые имена файлов
|
||||
|
||||
**Ограничения MVP:**
|
||||
|
||||
- Один пользователь (легко расширить до нескольких)
|
||||
- Только текстовые сообщения
|
||||
- Базовое форматирование без LLM
|
||||
- Локальный git (без конфликт-резолюции)
|
||||
|
||||
---
|
||||
|
||||
## 8. Roadmap после MVP
|
||||
|
||||
**v0.2:** YandexGPT интеграция для улучшенного форматирования
|
||||
**v0.3:** Поддержка голосовых сообщений + транскрипция
|
||||
**v0.4:** Векторный поиск по заметкам
|
||||
**v0.5:** Quartz автопубликация
|
||||
**v1.0:** Multi-user support с персональными vaults
|
||||
@@ -0,0 +1,428 @@
|
||||
## 1. Обзор системы
|
||||
|
||||
### 1.1 Назначение
|
||||
Система представляет собой Telegram бота для автоматизации создания и управления заметками в Obsidian с интеграцией в существующий Git + Quartz workflow.
|
||||
|
||||
### 1.2 Ключевые функции
|
||||
- Создание заметок из текстовых и голосовых сообщений
|
||||
- Семантический поиск по базе знаний
|
||||
- Дополнение существующих заметок
|
||||
- Автоматическое форматирование через YandexGPT
|
||||
- Интеграция с Git для версионирования
|
||||
- Автоматическая публикация через Quartz
|
||||
|
||||
### 1.3 Ограничения и допущения
|
||||
- Один пользователь (личное использование)
|
||||
- Развертывание на VPS с ограниченными ресурсами
|
||||
- Интеграция с существующей инфраструктурой Git + Quartz
|
||||
- Приоритет простоты разработки над масштабируемостью
|
||||
|
||||
## 2. Архитектурные решения
|
||||
|
||||
### 2.1 Выбор архитектурного стиля: Модульный монолит
|
||||
|
||||
**Обоснование:**
|
||||
- Простота разработки и развертывания
|
||||
- Оптимальное использование ресурсов VPS
|
||||
- Быстрая итерация для единственного пользователя
|
||||
- Легкость отладки и мониторинга
|
||||
|
||||
**Альтернативы рассмотренные:**
|
||||
- Микросервисная архитектура (отклонена из-за избыточной сложности)
|
||||
- Serverless функции (отклонена из-за холодного старта и ограничений по времени)
|
||||
|
||||
### 2.2 Технологический стек
|
||||
|
||||
**Основной язык:** Go 1.21+
|
||||
- Высокая производительность
|
||||
- Простое развертывание (единый бинарник)
|
||||
- Отличная поддержка конкурентности
|
||||
- Богатая экосистема для Telegram ботов
|
||||
|
||||
**Ключевые зависимости:**
|
||||
- `telegram-bot-api/v5` - Telegram Bot API
|
||||
- `yandexcloud-sdk-go` - YandexGPT API интеграция
|
||||
- `go-git/v5` - Git операции
|
||||
- `viper` - конфигурация
|
||||
- `logrus` - структурированное логирование
|
||||
|
||||
**Внешние сервисы:**
|
||||
- YandexGPT API (YandexGPT Lite/Pro, SpeechKit для голоса)
|
||||
- Telegram Bot API
|
||||
- Векторная база данных (Qdrant/Chroma)
|
||||
|
||||
## 3. Архитектура системы
|
||||
|
||||
### 3.1 Общая диаграмма системы
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Telegram User] --> B[Monolithic Bot App]
|
||||
B --> A
|
||||
B --> C[External Services]
|
||||
C --> B
|
||||
|
||||
subgraph "External Services"
|
||||
D[YandexGPT API]
|
||||
E[Vector DB]
|
||||
F[Telegram API]
|
||||
end
|
||||
|
||||
B --> G[File System]
|
||||
|
||||
subgraph "File System"
|
||||
H[Obsidian Vault]
|
||||
I[Git Repository]
|
||||
J[Vector Index]
|
||||
end
|
||||
|
||||
G --> K[Git Hooks]
|
||||
K --> L[Quartz Rebuild]
|
||||
|
||||
C --> D
|
||||
C --> E
|
||||
C --> F
|
||||
```
|
||||
|
||||
### 3.2 Компонентная диаграмма
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Monolithic Application"
|
||||
A[Bot Handler]
|
||||
B[HTTP Server]
|
||||
C[CLI Tools]
|
||||
|
||||
subgraph "Business Logic Layer"
|
||||
D[Note Service]
|
||||
E[Search Service]
|
||||
F[Voice Service]
|
||||
G[LLM Service]
|
||||
H[File Service]
|
||||
I[Git Service]
|
||||
end
|
||||
|
||||
subgraph "Infrastructure Layer"
|
||||
J[Config Manager]
|
||||
K[Logger]
|
||||
L[Cache]
|
||||
end
|
||||
end
|
||||
|
||||
A --> D
|
||||
A --> E
|
||||
A --> F
|
||||
B --> D
|
||||
B --> E
|
||||
B --> F
|
||||
C --> D
|
||||
C --> E
|
||||
C --> F
|
||||
|
||||
D --> G
|
||||
D --> H
|
||||
D --> I
|
||||
E --> G
|
||||
E --> H
|
||||
F --> G
|
||||
F --> H
|
||||
|
||||
G --> J
|
||||
H --> J
|
||||
I --> J
|
||||
G --> K
|
||||
H --> K
|
||||
I --> K
|
||||
```
|
||||
|
||||
### 3.3 Диаграмма потоков данных
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Telegram Message] --> B[Bot Handler]
|
||||
B --> C[Router]
|
||||
|
||||
C --> D[Create Note]
|
||||
C --> E[Search Notes]
|
||||
C --> F[Append Note]
|
||||
|
||||
D --> G[Format with YandexGPT]
|
||||
F --> H[Find & Update]
|
||||
|
||||
G --> I[Save File]
|
||||
E --> J[Vector Search]
|
||||
H --> I
|
||||
|
||||
I --> K[Git Commit]
|
||||
J --> K
|
||||
|
||||
K --> L[Quartz Rebuild]
|
||||
|
||||
subgraph "Message Processing"
|
||||
A
|
||||
B
|
||||
C
|
||||
end
|
||||
|
||||
subgraph "Note Operations"
|
||||
D
|
||||
E
|
||||
F
|
||||
end
|
||||
|
||||
subgraph "Processing"
|
||||
G
|
||||
H
|
||||
J
|
||||
end
|
||||
|
||||
subgraph "Storage"
|
||||
I
|
||||
K
|
||||
L
|
||||
end
|
||||
```
|
||||
|
||||
## 4. Детальное описание компонентов
|
||||
|
||||
### 4.1 Presentation Layer
|
||||
|
||||
**Bot Handler**
|
||||
- Ответственность: Обработка Telegram API, маршрутизация команд
|
||||
- Интерфейсы: Telegram Bot API
|
||||
- Зависимости: Router, Context Manager
|
||||
|
||||
**HTTP Server** (опциональный)
|
||||
- Ответственность: Health checks, метрики, webhook endpoint
|
||||
- Интерфейсы: HTTP REST API
|
||||
- Зависимости: Monitoring tools
|
||||
|
||||
### 4.2 Business Logic Layer
|
||||
|
||||
**Note Service**
|
||||
- Ответственность: Создание, обновление заметок, управление жизненным циклом
|
||||
- Интерфейсы: Note CRUD операции
|
||||
- Зависимости: LLM Service, File Service, Git Service
|
||||
|
||||
**Search Service**
|
||||
- Ответственность: Векторный поиск, индексация, семантический анализ
|
||||
- Интерфейсы: Search API, Indexing API
|
||||
- Зависимости: Vector Database, Embedding Service
|
||||
|
||||
**Voice Service**
|
||||
- Ответственность: Обработка голосовых сообщений, транскрипция
|
||||
- Интерфейсы: Audio processing API
|
||||
- Зависимости: Yandex SpeechKit API, File Service
|
||||
|
||||
**LLM Service**
|
||||
- Ответственность: Интеграция с YandexGPT, форматирование, генерация контента
|
||||
- Интерфейсы: Text processing API
|
||||
- Зависимости: YandexGPT API
|
||||
|
||||
### 4.3 Infrastructure Layer
|
||||
|
||||
**File Service**
|
||||
- Ответственность: Файловые операции, управление Obsidian vault
|
||||
- Интерфейсы: File system operations
|
||||
- Зависимости: OS file system
|
||||
|
||||
**Git Service**
|
||||
- Ответственность: Version control, автоматические коммиты
|
||||
- Интерфейсы: Git operations API
|
||||
- Зависимости: Git repository
|
||||
|
||||
## 5. Интеграционная архитектура
|
||||
|
||||
### 5.1 Внешние интеграции
|
||||
|
||||
**YandexGPT API**
|
||||
- Назначение: YandexGPT Lite/Pro для форматирования, SpeechKit для транскрипции
|
||||
- Протокол: HTTPS REST API
|
||||
- Аутентификация: API Key (IAM токен)
|
||||
- Обработка ошибок: Retry with exponential backoff
|
||||
- Модели: yandexgpt-lite, yandexgpt-pro
|
||||
|
||||
**Telegram Bot API**
|
||||
- Назначение: Получение сообщений, отправка ответов
|
||||
- Протокол: HTTPS REST API / WebHooks
|
||||
- Аутентификация: Bot Token
|
||||
- Режим работы: Long polling (рекомендуется для VPS)
|
||||
|
||||
**Vector Database**
|
||||
- Назначение: Семантический поиск по заметкам
|
||||
- Варианты: Qdrant (HTTP API) или Chroma (HTTP API)
|
||||
- Протокол: HTTPS REST API
|
||||
- Данные: Embeddings + метаданные заметок
|
||||
|
||||
### 5.2 Файловая система
|
||||
|
||||
**Obsidian Vault Structure**
|
||||
```
|
||||
vault/
|
||||
├── notes/ # Основные заметки
|
||||
├── daily/ # Ежедневные заметки
|
||||
├── templates/ # Шаблоны
|
||||
├── attachments/ # Медиа файлы
|
||||
└── .obsidian/ # Конфигурация Obsidian
|
||||
```
|
||||
|
||||
**Git Integration**
|
||||
- Автоматические коммиты после каждого изменения
|
||||
- Meaningful commit messages
|
||||
- Direct push в main branch
|
||||
- Git hooks для триггера Quartz rebuild
|
||||
|
||||
## 6. Данные и хранение
|
||||
|
||||
### 6.1 Модель данных
|
||||
|
||||
**Note Entity**
|
||||
```
|
||||
Note {
|
||||
ID: UUID
|
||||
Title: string
|
||||
Content: string (Markdown)
|
||||
Filename: string
|
||||
Created: timestamp
|
||||
Updated: timestamp
|
||||
Tags: []string
|
||||
Links: []string
|
||||
Embedding: []float32
|
||||
}
|
||||
```
|
||||
|
||||
**User Context**
|
||||
```
|
||||
UserContext {
|
||||
UserID: int64
|
||||
State: enum (idle, creating_note, appending, searching)
|
||||
Data: map[string]interface{}
|
||||
LastActivity: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Хранение данных
|
||||
|
||||
**Файловая система**
|
||||
- Obsidian заметки: Markdown файлы в vault directory
|
||||
- Конфигурация: .env файл или environment variables
|
||||
- Логи: Rotated log files или stdout для containerized deployment
|
||||
|
||||
**Временное хранение**
|
||||
- User contexts: In-memory map (single user)
|
||||
- Cache: In-memory или embedded BoltDB
|
||||
- Vector embeddings: External vector database
|
||||
|
||||
## 7. Развертывание и операционные аспекты
|
||||
|
||||
### 7.1 Архитектура развертывания
|
||||
|
||||
```
|
||||
VPS Server
|
||||
├── Application Binary
|
||||
├── Configuration Files
|
||||
├── Obsidian Vault (Git Repository)
|
||||
├── Vector Database (если локальная)
|
||||
└── Monitoring & Logs
|
||||
```
|
||||
|
||||
### 7.2 Масштабирование
|
||||
|
||||
**Текущие ограничения:**
|
||||
- Один пользователь
|
||||
- Один экземпляр приложения
|
||||
- Локальные файловые операции
|
||||
|
||||
**Возможности роста:**
|
||||
- Horizontal scaling: добавление Redis для shared state
|
||||
- Vertical scaling: увеличение ресурсов VPS
|
||||
- Service extraction: выделение vector search в отдельный сервис
|
||||
|
||||
### 7.3 Мониторинг и наблюдаемость
|
||||
|
||||
**Логирование:**
|
||||
- Structured logging (JSON format)
|
||||
- Log levels: ERROR, WARN, INFO, DEBUG
|
||||
- Log rotation для экономии места
|
||||
|
||||
**Метрики:**
|
||||
- Application health status
|
||||
- Message processing time
|
||||
- YandexGPT API response times
|
||||
- Git operation success/failure rates
|
||||
|
||||
**Алерты:**
|
||||
- Application down
|
||||
- High error rate
|
||||
- YandexGPT API quota exceeded
|
||||
- Git push failures
|
||||
|
||||
## 8. Безопасность
|
||||
|
||||
### 8.1 Аутентификация и авторизация
|
||||
- Telegram User ID whitelist (только владелец)
|
||||
- API keys в environment variables
|
||||
- No public endpoints кроме Telegram webhook
|
||||
|
||||
### 8.2 Защита данных
|
||||
- API keys не в коде
|
||||
- Логи не содержат sensitive данные
|
||||
- HTTPS для всех внешних API calls
|
||||
- Git repository может быть private
|
||||
|
||||
## 9. Качественные атрибуты
|
||||
|
||||
### 9.1 Производительность
|
||||
- Время отклика бота: < 2 секунды для текстовых сообщений
|
||||
- Время создания заметки: < 5 секунд включая YandexGPT обработку
|
||||
- Голосовые сообщения: < 10 секунд для транскрипции и создания заметки
|
||||
|
||||
### 9.2 Надежность
|
||||
- Graceful degradation при недоступности YandexGPT API
|
||||
- Retry mechanisms для внешних API
|
||||
- Data consistency через atomic git operations
|
||||
|
||||
### 9.3 Maintainability
|
||||
- Четкое разделение ответственности между слоями
|
||||
- Интерфейсы для всех основных компонентов
|
||||
- Comprehensive logging для отладки
|
||||
- Configuration management через environment variables
|
||||
|
||||
## 10. Риски и ограничения
|
||||
|
||||
### 10.1 Технические риски
|
||||
- **YandexGPT API limits:** Rate limiting может замедлить обработку
|
||||
- **Vector DB performance:** Поиск может замедляться с ростом количества заметок
|
||||
- **VPS resources:** Ограниченная память и CPU для vector operations
|
||||
|
||||
### 10.2 Операционные риски
|
||||
- **Single point of failure:** Монолитная архитектура
|
||||
- **Manual deployment:** Отсутствие автоматизированного CI/CD
|
||||
- **Backup dependency:** Зависимость от Git для backup заметок
|
||||
|
||||
### 10.3 Митигация рисков
|
||||
- Monitoring и alerting для раннего обнаружения проблем
|
||||
- Регулярные backups Git repository
|
||||
- Graceful fallbacks при недоступности внешних сервисов
|
||||
- Resource monitoring для предотвращения исчерпания ресурсов VPS
|
||||
|
||||
## 11. Конфигурация YandexGPT
|
||||
|
||||
### 11.1 API Ключи и аутентификация
|
||||
- **IAM Token:** Основной способ аутентификации для YandexGPT API
|
||||
- **API Key:** Альтернативный способ для некоторых сервисов
|
||||
- **Folder ID:** Идентификатор каталога в Yandex Cloud
|
||||
|
||||
### 11.2 Модели YandexGPT
|
||||
- **yandexgpt-lite:** Быстрая модель для базового форматирования
|
||||
- **yandexgpt-pro:** Продвинутая модель для сложных задач
|
||||
- **speechkit:** Для обработки голосовых сообщений
|
||||
|
||||
### 11.3 Переменные окружения
|
||||
```
|
||||
YANDEX_API_KEY=your_api_key
|
||||
YANDEX_FOLDER_ID=your_folder_id
|
||||
YANDEX_IAM_TOKEN=your_iam_token
|
||||
TELEGRAM_BOT_TOKEN=your_bot_token
|
||||
VAULT_PATH=/path/to/obsidian/vault
|
||||
```
|
||||
@@ -0,0 +1,857 @@
|
||||
|
||||
## Шаг 1: Настройка проекта и структуры
|
||||
|
||||
### 1.1 Инициализация проекта
|
||||
|
||||
```bash
|
||||
mkdir obsidian-telegram-bot
|
||||
cd obsidian-telegram-bot
|
||||
go mod init github.com/yourusername/obsidian-telegram-bot
|
||||
|
||||
# Создание структуры
|
||||
mkdir -p cmd/bot
|
||||
mkdir -p internal/{bot,note,git,config}
|
||||
mkdir -p configs
|
||||
mkdir -p vault/notes
|
||||
```
|
||||
|
||||
### 1.2 Установка зависимостей
|
||||
|
||||
```bash
|
||||
go get github.com/go-telegram-bot-api/telegram-bot-api/v5
|
||||
go get github.com/go-git/go-git/v5
|
||||
go get github.com/spf13/viper
|
||||
go get github.com/sirupsen/logrus
|
||||
```
|
||||
|
||||
### 1.3 Базовый конфиг
|
||||
|
||||
```yaml
|
||||
# configs/config.yaml
|
||||
telegram_token: ""
|
||||
vault_path: "./vault/notes"
|
||||
allowed_user_ids: []
|
||||
|
||||
git:
|
||||
sync_interval: "30s"
|
||||
max_queue_size: 100
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 2: Конфигурация и логирование
|
||||
|
||||
### 2.1 Config service
|
||||
|
||||
```go
|
||||
// internal/config/config.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
TelegramToken string `mapstructure:"telegram_token"`
|
||||
VaultPath string `mapstructure:"vault_path"`
|
||||
AllowedUserIDs []int64 `mapstructure:"allowed_user_ids"`
|
||||
Git GitConfig `mapstructure:"git"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
type GitConfig struct {
|
||||
SyncInterval string `mapstructure:"sync_interval"`
|
||||
MaxQueueSize int `mapstructure:"max_queue_size"`
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
}
|
||||
|
||||
func Load(configPath string) (*Config, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (c *Config) GetSyncInterval() time.Duration {
|
||||
duration, err := time.ParseDuration(c.Git.SyncInterval)
|
||||
if err != nil {
|
||||
return 30 * time.Second // default
|
||||
}
|
||||
return duration
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Инициализация логирования
|
||||
|
||||
```go
|
||||
// internal/config/logger.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func SetupLogger(level string) {
|
||||
logLevel, err := logrus.ParseLevel(level)
|
||||
if err != nil {
|
||||
logLevel = logrus.InfoLevel
|
||||
}
|
||||
|
||||
logrus.SetLevel(logLevel)
|
||||
logrus.SetFormatter(&logrus.JSONFormatter{})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 3: File Service (атомарные операции с файлами)
|
||||
|
||||
```go
|
||||
// internal/note/file_service.go
|
||||
package note
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
vaultPath string
|
||||
}
|
||||
|
||||
func NewFileService(vaultPath string) *FileService {
|
||||
return &FileService{vaultPath: vaultPath}
|
||||
}
|
||||
|
||||
func (f *FileService) CreateNote(text string) (string, error) {
|
||||
// Создаем директорию если не существует
|
||||
if err := os.MkdirAll(f.vaultPath, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create vault directory: %w", err)
|
||||
}
|
||||
|
||||
// Генерируем имя файла
|
||||
filename := f.generateFilename(text)
|
||||
filePath := filepath.Join(f.vaultPath, filename)
|
||||
|
||||
// Проверяем уникальность
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
filename = f.generateUniqueFilename(text)
|
||||
filePath = filepath.Join(f.vaultPath, filename)
|
||||
}
|
||||
|
||||
// Форматируем содержимое
|
||||
content := f.formatMarkdown(text)
|
||||
|
||||
// Атомарная запись
|
||||
if err := f.atomicWrite(filePath, content); err != nil {
|
||||
return "", fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
func (f *FileService) AppendToNote(filename, text string) error {
|
||||
filePath := filepath.Join(f.vaultPath, filename)
|
||||
|
||||
// Читаем существующий контент
|
||||
existingContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read existing note: %w", err)
|
||||
}
|
||||
|
||||
// Добавляем новый контент
|
||||
newContent := string(existingContent) + "\n\n" + text
|
||||
|
||||
// Атомарная перезапись
|
||||
return f.atomicWrite(filePath, newContent)
|
||||
}
|
||||
|
||||
func (f *FileService) generateFilename(text string) string {
|
||||
title := f.extractTitle(text)
|
||||
slug := f.slugify(title)
|
||||
timestamp := time.Now().Format("2006-01-02-1504")
|
||||
|
||||
return fmt.Sprintf("%s-%s.md", timestamp, slug)
|
||||
}
|
||||
|
||||
func (f *FileService) generateUniqueFilename(text string) string {
|
||||
title := f.extractTitle(text)
|
||||
slug := f.slugify(title)
|
||||
timestamp := time.Now().Format("2006-01-02-150405") // Добавляем секунды
|
||||
|
||||
return fmt.Sprintf("%s-%s.md", timestamp, slug)
|
||||
}
|
||||
|
||||
func (f *FileService) extractTitle(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
if len(lines) == 0 {
|
||||
return "note"
|
||||
}
|
||||
|
||||
// Берем первую строку, ограничиваем длину
|
||||
title := strings.TrimSpace(lines[0])
|
||||
if len(title) > 50 {
|
||||
title = title[:50]
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
return "note"
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
func (f *FileService) slugify(text string) string {
|
||||
// Убираем спецсимволы
|
||||
reg := regexp.MustCompile(`[^a-zA-Z0-9\s]+`)
|
||||
clean := reg.ReplaceAllString(text, "")
|
||||
|
||||
// Разбиваем на слова
|
||||
words := strings.Fields(strings.ToLower(clean))
|
||||
|
||||
// Берем первые 3 слова
|
||||
if len(words) > 3 {
|
||||
words = words[:3]
|
||||
}
|
||||
|
||||
if len(words) == 0 {
|
||||
return "note"
|
||||
}
|
||||
|
||||
return strings.Join(words, "-")
|
||||
}
|
||||
|
||||
func (f *FileService) formatMarkdown(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
// Добавляем YAML frontmatter
|
||||
frontmatter := fmt.Sprintf(`---
|
||||
created: %s
|
||||
tags: [telegram-note]
|
||||
---
|
||||
|
||||
`, time.Now().Format("2006-01-02T15:04:05Z07:00"))
|
||||
|
||||
// Если первая строка не заголовок - делаем её заголовком
|
||||
if len(lines) > 0 && !strings.HasPrefix(strings.TrimSpace(lines[0]), "#") {
|
||||
lines[0] = "# " + strings.TrimSpace(lines[0])
|
||||
}
|
||||
|
||||
return frontmatter + strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (f *FileService) atomicWrite(filePath, content string) error {
|
||||
// Записываем во временный файл
|
||||
tempFile := filePath + ".tmp"
|
||||
|
||||
file, err := os.Create(tempFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = file.WriteString(content)
|
||||
closeErr := file.Close()
|
||||
|
||||
if err != nil {
|
||||
os.Remove(tempFile)
|
||||
return err
|
||||
}
|
||||
|
||||
if closeErr != nil {
|
||||
os.Remove(tempFile)
|
||||
return closeErr
|
||||
}
|
||||
|
||||
// Атомарное переименование
|
||||
return os.Rename(tempFile, filePath)
|
||||
}
|
||||
|
||||
func (f *FileService) FindNotesByPartialName(query string) ([]string, error) {
|
||||
files, err := os.ReadDir(f.vaultPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matches []string
|
||||
query = strings.ToLower(query)
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := strings.ToLower(file.Name())
|
||||
if strings.Contains(filename, query) {
|
||||
matches = append(matches, file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// Ограничиваем до 5 результатов
|
||||
if len(matches) > 5 {
|
||||
matches = matches[:5]
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func (f *FileService) ListRecentNotes(limit int) ([]string, error) {
|
||||
files, err := os.ReadDir(f.vaultPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Сортируем по времени модификации (новые первые)
|
||||
var fileInfos []os.FileInfo
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fileInfos = append(fileInfos, info)
|
||||
}
|
||||
|
||||
// Простая сортировка по времени модификации
|
||||
for i := 0; i < len(fileInfos)-1; i++ {
|
||||
for j := i + 1; j < len(fileInfos); j++ {
|
||||
if fileInfos[i].ModTime().Before(fileInfos[j].ModTime()) {
|
||||
fileInfos[i], fileInfos[j] = fileInfos[j], fileInfos[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for i, info := range fileInfos {
|
||||
if i >= limit {
|
||||
break
|
||||
}
|
||||
result = append(result, info.Name())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 4: Git Service (асинхронная синхронизация)
|
||||
|
||||
```go
|
||||
// internal/git/service.go
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *git.Repository
|
||||
worktree *git.Worktree
|
||||
fileQueue chan string
|
||||
stopChan chan struct{}
|
||||
vaultPath string
|
||||
syncInterval time.Duration
|
||||
}
|
||||
|
||||
func NewService(vaultPath string, syncInterval time.Duration) (*Service, error) {
|
||||
// Инициализируем или открываем git репозиторий
|
||||
repo, err := git.PlainOpen(vaultPath)
|
||||
if err != nil {
|
||||
// Если репозитория нет, создаем новый
|
||||
repo, err = git.PlainInit(vaultPath, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init git repo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get worktree: %w", err)
|
||||
}
|
||||
|
||||
return &Service{
|
||||
repo: repo,
|
||||
worktree: worktree,
|
||||
fileQueue: make(chan string, 100), // Буфер на 100 файлов
|
||||
stopChan: make(chan struct{}),
|
||||
vaultPath: vaultPath,
|
||||
syncInterval: syncInterval,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Start() {
|
||||
go s.syncWorker()
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
close(s.stopChan)
|
||||
}
|
||||
|
||||
func (s *Service) QueueFile(filename string) {
|
||||
select {
|
||||
case s.fileQueue <- filename:
|
||||
logrus.Debugf("File queued for git sync: %s", filename)
|
||||
default:
|
||||
logrus.Warnf("Git queue full, file will be synced later: %s", filename)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) syncWorker() {
|
||||
ticker := time.NewTicker(s.syncInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
var pendingFiles []string
|
||||
|
||||
for {
|
||||
select {
|
||||
case filename := <-s.fileQueue:
|
||||
pendingFiles = append(pendingFiles, filename)
|
||||
|
||||
case <-ticker.C:
|
||||
if len(pendingFiles) > 0 {
|
||||
s.commitBatch(pendingFiles)
|
||||
pendingFiles = nil
|
||||
}
|
||||
|
||||
case <-s.stopChan:
|
||||
// Последний sync перед выходом
|
||||
if len(pendingFiles) > 0 {
|
||||
s.commitBatch(pendingFiles)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) commitBatch(files []string) {
|
||||
logrus.Infof("Starting git sync for %d files", len(files))
|
||||
|
||||
// Добавляем файлы в индекс
|
||||
for _, filename := range files {
|
||||
filePath := filepath.Join("notes", filename)
|
||||
_, err := s.worktree.Add(filePath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to add file to git: %s, error: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем коммит
|
||||
message := s.generateCommitMessage(files)
|
||||
commit, err := s.worktree.Commit(message, &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: "Telegram Bot",
|
||||
Email: "bot@telegram",
|
||||
When: time.Now(),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to commit: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
logrus.Infof("Created commit: %s", commit.String())
|
||||
|
||||
// Пушим если есть remote
|
||||
s.tryPush()
|
||||
}
|
||||
|
||||
func (s *Service) generateCommitMessage(files []string) string {
|
||||
if len(files) == 1 {
|
||||
return fmt.Sprintf("Add note: %s", files[0])
|
||||
}
|
||||
return fmt.Sprintf("Add %d notes via Telegram bot", len(files))
|
||||
}
|
||||
|
||||
func (s *Service) tryPush() {
|
||||
// Проверяем есть ли remote
|
||||
remotes, err := s.repo.Remotes()
|
||||
if err != nil || len(remotes) == 0 {
|
||||
logrus.Debug("No remotes configured, skipping push")
|
||||
return
|
||||
}
|
||||
|
||||
// Пытаемся запушить
|
||||
err = s.repo.Push(&git.PushOptions{})
|
||||
if err != nil {
|
||||
if err == git.NoErrAlreadyUpToDate {
|
||||
logrus.Debug("Git: already up to date")
|
||||
} else {
|
||||
logrus.Errorf("Failed to push: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logrus.Info("Successfully pushed to remote")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 5: Bot Handler (Telegram интеграция)
|
||||
|
||||
```go
|
||||
// internal/bot/handler.go
|
||||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"your-module/internal/config"
|
||||
"your-module/internal/git"
|
||||
"your-module/internal/note"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
bot *tgbotapi.BotAPI
|
||||
fileService *note.FileService
|
||||
gitService *git.Service
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewHandler(bot *tgbotapi.BotAPI, fileService *note.FileService, gitService *git.Service, cfg *config.Config) *Handler {
|
||||
return &Handler{
|
||||
bot: bot,
|
||||
fileService: fileService,
|
||||
gitService: gitService,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) HandleUpdates() {
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates := h.bot.GetUpdatesChan(u)
|
||||
|
||||
for update := range updates {
|
||||
if update.Message == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
go h.handleUpdate(update)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleUpdate(update tgbotapi.Update) {
|
||||
// Проверяем авторизацию
|
||||
if !h.isUserAllowed(update.Message.From.ID) {
|
||||
h.sendMessage(update.Message.Chat.ID, "❌ Access denied")
|
||||
logrus.Warnf("Unauthorized access attempt from user %d", update.Message.From.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// Обрабатываем команды
|
||||
if update.Message.IsCommand() {
|
||||
h.handleCommand(update)
|
||||
} else {
|
||||
// Обычное сообщение - создаем заметку
|
||||
h.handleNewNote(update, update.Message.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleCommand(update tgbotapi.Update) {
|
||||
command := update.Message.Command()
|
||||
args := update.Message.CommandArguments()
|
||||
|
||||
switch command {
|
||||
case "start":
|
||||
h.handleStart(update)
|
||||
case "new":
|
||||
if args == "" {
|
||||
h.sendMessage(update.Message.Chat.ID, "❌ Usage: /new <text>")
|
||||
return
|
||||
}
|
||||
h.handleNewNote(update, args)
|
||||
case "append":
|
||||
h.handleAppend(update, args)
|
||||
case "list":
|
||||
h.handleList(update)
|
||||
default:
|
||||
h.sendMessage(update.Message.Chat.ID, "❌ Unknown command. Available: /new, /append, /list")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleStart(update tgbotapi.Update) {
|
||||
message := `🤖 *Obsidian Telegram Bot*
|
||||
|
||||
Available commands:
|
||||
• \`/new <text>\` - Create new note
|
||||
• \`/append <name>\` - Append to existing note
|
||||
• \`/list\` - Show recent notes
|
||||
• Just send text to create a note
|
||||
|
||||
Your notes are automatically synced to Git!`
|
||||
|
||||
h.sendMessage(update.Message.Chat.ID, message)
|
||||
}
|
||||
|
||||
func (h *Handler) handleNewNote(update tgbotapi.Update, text string) {
|
||||
chatID := update.Message.Chat.ID
|
||||
|
||||
if strings.TrimSpace(text) == "" {
|
||||
h.sendMessage(chatID, "❌ Cannot create empty note")
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем файл
|
||||
filename, err := h.fileService.CreateNote(text)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to create note for user %d: %v", update.Message.From.ID, err)
|
||||
h.sendMessage(chatID, "❌ Failed to create note. Please try again.")
|
||||
return
|
||||
}
|
||||
|
||||
// Мгновенный ответ пользователю
|
||||
message := fmt.Sprintf("✅ Note created: `%s`", filename)
|
||||
h.sendMessage(chatID, message)
|
||||
|
||||
// Асинхронная синхронизация с git
|
||||
h.gitService.QueueFile(filename)
|
||||
|
||||
logrus.Infof("Note created by user %d: %s", update.Message.From.ID, filename)
|
||||
}
|
||||
|
||||
func (h *Handler) handleAppend(update tgbotapi.Update, args string) {
|
||||
chatID := update.Message.Chat.ID
|
||||
|
||||
if args == "" {
|
||||
h.sendMessage(chatID, "❌ Usage: /append <partial_filename>")
|
||||
return
|
||||
}
|
||||
|
||||
// Ищем файлы по частичному имени
|
||||
matches, err := h.fileService.FindNotesByPartialName(args)
|
||||
if err != nil {
|
||||
h.sendMessage(chatID, "❌ Error searching notes")
|
||||
return
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
h.sendMessage(chatID, fmt.Sprintf("❌ No notes found matching: %s", args))
|
||||
return
|
||||
}
|
||||
|
||||
if len(matches) == 1 {
|
||||
// Найден один файл - просим текст для добавления
|
||||
h.sendMessage(chatID, fmt.Sprintf("📝 Found: `%s`\nSend text to append:", matches[0]))
|
||||
// TODO: Реализовать состояние для следующего сообщения
|
||||
return
|
||||
}
|
||||
|
||||
// Несколько совпадений - показываем список
|
||||
message := fmt.Sprintf("Found %d matches:\n", len(matches))
|
||||
for i, match := range matches {
|
||||
message += fmt.Sprintf("%d. `%s`\n", i+1, match)
|
||||
}
|
||||
message += "\nBe more specific or use exact filename with /append"
|
||||
|
||||
h.sendMessage(chatID, message)
|
||||
}
|
||||
|
||||
func (h *Handler) handleList(update tgbotapi.Update) {
|
||||
chatID := update.Message.Chat.ID
|
||||
|
||||
notes, err := h.fileService.ListRecentNotes(5)
|
||||
if err != nil {
|
||||
h.sendMessage(chatID, "❌ Error listing notes")
|
||||
return
|
||||
}
|
||||
|
||||
if len(notes) == 0 {
|
||||
h.sendMessage(chatID, "📝 No notes found")
|
||||
return
|
||||
}
|
||||
|
||||
message := "📚 *Recent notes:*\n"
|
||||
for i, note := range notes {
|
||||
message += fmt.Sprintf("%d. `%s`\n", i+1, note)
|
||||
}
|
||||
|
||||
h.sendMessage(chatID, message)
|
||||
}
|
||||
|
||||
func (h *Handler) isUserAllowed(userID int64) bool {
|
||||
for _, allowedID := range h.config.AllowedUserIDs {
|
||||
if userID == allowedID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handler) sendMessage(chatID int64, text string) {
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
msg.ParseMode = "Markdown"
|
||||
|
||||
if _, err := h.bot.Send(msg); err != nil {
|
||||
logrus.Errorf("Failed to send message: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 6: Main application
|
||||
|
||||
```go
|
||||
// cmd/bot/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"your-module/internal/bot"
|
||||
"your-module/internal/config"
|
||||
"your-module/internal/git"
|
||||
"your-module/internal/note"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "configs/config.yaml", "Path to config file")
|
||||
flag.Parse()
|
||||
|
||||
// Загружаем конфигурацию
|
||||
cfg, err := config.Load(*configPath)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Настраиваем логирование
|
||||
config.SetupLogger(cfg.Logging.Level)
|
||||
|
||||
// Инициализируем Telegram бота
|
||||
telegramBot, err := tgbotapi.NewBotAPI(cfg.TelegramToken)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed to create Telegram bot: %v", err)
|
||||
}
|
||||
|
||||
logrus.Infof("Authorized on account %s", telegramBot.Self.UserName)
|
||||
|
||||
// Инициализируем сервисы
|
||||
fileService := note.NewFileService(cfg.VaultPath)
|
||||
|
||||
gitService, err := git.NewService(cfg.VaultPath, cfg.GetSyncInterval())
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed to create git service: %v", err)
|
||||
}
|
||||
|
||||
// Запускаем git синхронизацию
|
||||
gitService.Start()
|
||||
|
||||
// Создаем обработчик бота
|
||||
handler := bot.NewHandler(telegramBot, fileService, gitService, cfg)
|
||||
|
||||
// Graceful shutdown
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-c
|
||||
logrus.Info("Shutting down...")
|
||||
|
||||
// Останавливаем git service
|
||||
gitService.Stop()
|
||||
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
logrus.Info("Bot started")
|
||||
|
||||
// Запускаем обработку сообщений
|
||||
handler.HandleUpdates()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 7: Makefile и запуск
|
||||
|
||||
```makefile
|
||||
# Makefile
|
||||
.PHONY: build run clean test
|
||||
|
||||
APP_NAME=obsidian-bot
|
||||
BUILD_DIR=bin
|
||||
|
||||
build:
|
||||
go build -o $(BUILD_DIR)/$(APP_NAME) cmd/bot/main.go
|
||||
|
||||
run: build
|
||||
./$(BUILD_DIR)/$(APP_NAME) -config configs/config.yaml
|
||||
|
||||
dev:
|
||||
go run cmd/bot/main.go -config configs/config.yaml
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
docker-build:
|
||||
docker build -t $(APP_NAME) .
|
||||
|
||||
docker-run: docker-build
|
||||
docker run --rm -v $(PWD)/configs:/app/configs -v $(PWD)/vault:/app/vault $(APP_NAME)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Последовательность реализации:
|
||||
|
||||
1. **Шаг 1-2**: Настройка проекта и конфигурации (30 минут)
|
||||
2. **Шаг 3**: File Service - основная логика работы с файлами (1-2 часа)
|
||||
3. **Шаг 4**: Git Service - асинхронная синхронизация (1 час)
|
||||
4. **Шаг 5**: Bot Handler - интеграция с Telegram (1 час)
|
||||
5. **Шаг 6**: Main app и тестирование (30 минут)
|
||||
|
||||
**Общее время: 4-5 часов** для полнофункционального MVP.
|
||||
|
||||
После реализации нужно будет:
|
||||
|
||||
- Создать Telegram бота через @BotFather
|
||||
- Настроить config.yaml с токенами
|
||||
- Инициализировать git репозиторий в vault/
|
||||
- Добавить свой Telegram User ID в whitelist
|
||||
Reference in New Issue
Block a user