vault backup: 2025-08-08 09:44:48

This commit is contained in:
Andrey Epifancev
2025-08-08 09:44:48 +04:00
parent fde9c6277a
commit aeb5ad9560
2 changed files with 1033 additions and 50 deletions

View File

@@ -1,94 +1,220 @@
---
tags:
- mvp
- teleram-bot
- LLM
---
---
tags:
- mvp
- telegram-bot
- obsidian
- go
---
## 1. Цели MVP
- Принимать текстовые сообщения в Telegram.
- Создавать заметки в Obsidian Vault в формате Markdown.
- Автоматически коммитить и пушить изменения в Git-репозиторий.
- Поддерживать базовое форматирование текста через YandexGPT.
- Поддержка одного пользователя.
- Принимать текстовые сообщения в Telegram
- Создавать заметки в Obsidian Vault в формате Markdown
- Асинхронно синхронизировать изменения с Git-репозиторием
- Поддерживать базовое форматирование текста (без LLM)
- Поддержка whitelist пользователей
> **Не включаем на первом этапе:**
> Голосовые сообщения, векторный поиск, Quartz-публикацию.
> Голосовые сообщения, векторный поиск, Quartz-публикация, LLM интеграция
---
## 2. Компоненты MVP
### 2.1 Bot Handler (Go + Telegram API)
- Используем `telegram-bot-api/v5`.
- Используем `telegram-bot-api/v5`
- Поддерживаем команды:
- `/new <текст>` — создать новую заметку.
- `/append <имя>` — дописать к существующей заметке (по exact match).
- `/list` — список последних 5 заметок.
- `/new <текст>` — создать новую заметку
- `/append <частичное_имя>` — дописать к существующей заметке (fuzzy search)
- `/list` — список последних 5 заметок
- Whitelist авторизация по Telegram User ID
- Graceful shutdown с завершением pending операций
### 2.2 Note Service
- Создание Markdown-файлов в директории `vault/notes`.
- Простая генерация имени файла по дате + заголовку.
- Добавление timestamps в YAML frontmatter.
- Вызов LLM Service для легкого форматирования (через YandexGPT API).
### 2.3 LLM Service (YandexGPT)
- YandexGPT форматирует Markdown (добавляет заголовки, убирает лишние пробелы).
- Транскрипции и поиск по эмбеддингам — **не реализуем** на MVP.
- Создание Markdown-файлов в директории `vault/notes`
- Умная генерация имени файла: `YYYY-MM-DD-HHMM-slug.md`
- Добавление timestamps в YAML frontmatter
- Базовое форматирование (первая строка = заголовок)
- Fuzzy search для поиска существующих заметок
### 2.4 Git Service
- Используем `go-git/v5`.
- После создания/обновления заметки:
1. `git add <file>`
2. `git commit -m "add note <title>"`
3. `git push`
### 2.3 Git Service (Асинхронный)
### 2.5 File Service
- Создает файлы в `vault/notes`.
- Проверяет уникальность имени.
- Обеспечивает atomic save.
- Используем `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. Поток обработки сообщений MVP
## 3. Поток обработки сообщений
```mermaid
graph TB
A[User] --> B[Telegram Bot]
B --> C[Bot Handler]
B --> C[Auth Check]
C --> D["/new text"]
D --> E[Note Service]
E --> F[LLM Service format]
F --> G[File Service]
G --> H[Git Service]
H --> I["Заметка создана: filename"]
I --> A
E --> F[File Service: Save]
F --> G["✅ Instant Response"]
G --> A
subgraph "MVP Flow"
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. Технологический стек MVP
## 4. Технологический стек
**Основные зависимости:**
- `telegram-bot-api/v5` - Telegram Bot API
- `yandexcloud-sdk-go` - YandexGPT API интеграция
- `go-git/v5` - Git операции
- `viper` - конфигурация
- `logrus` - структурированное логирование
**Внешние сервисы:**
- YandexGPT API (YandexGPT Lite/Pro)
- Telegram Bot API
- `telegram-bot-api/v5` - Telegram Bot API
- `go-git/v5` - Git операции
- `viper` - конфигурация
- `logrus` - структурированное логирование
**Конфигурация:**
- `YANDEX_API_KEY` - API ключ для YandexGPT
- `TELEGRAM_BOT_TOKEN` - токен Telegram бота
- `VAULT_PATH` - путь к Obsidian vault
```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

View File

@@ -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