Как стать автором
Поиск
Написать публикацию
Обновить

Как сделать идеальный lead gateway-бот на Go для Telegram: опыт и открытый исходник

Кавайная anime-привратница с чёрным котом
Кавайная anime-привратница с чёрным котом

Сегодня Telegram — одна из самых эффективных площадок для сбора лидов и создания закрытых сообществ. Если вы хотите, чтобы подписчики проходили через автоматическую регистрацию, подтверждали вступление в приватный канал, оставляли контакты и попадали в вашу CRM — всё это можно реализовать одним ботом на Go.

В этой статье я расскажу о реальном проекте Telegram Gateway Bot:

  • Поделюсь готовой архитектурой

  • Покажу, как настроить миграции и хранение в MySQL

  • Объясню, как сделать удобную интеграцию с CRM

  • Расскажу про systemd-деплой и best practices

  • И немного про "маскотов" и визуальный стиль

Кратко о возможностях

  • Интерактивная регистрация и сбор e-mail/телефона

  • Кнопка-приглашение в приватный канал/группу

  • Проверка членства через Telegram API

  • Хранение всех лидов в MySQL

  • Массовая рассылка (только для админов)

  • Интеграция с CRM через webhook/REST API

  • Удобные миграции через Goose

  • Запуск в продакшене как systemd-демон

  • Поддержка .env для конфигов

  • Красивый маскот — anime gatekeeper-сорceress

Исходный код

Структура проекта

.
├── README.MD
├── assets
│   └── img
├── cmd
│   ├── main.go
│   └── migrate.go
├── go.mod
├── go.sum
├── internal
│   ├── components
│   │   ├── config
│   │   │   └── config.go
│   │   ├── crm
│   │   │   └── crm.go
│   │   └── db
│   │       └── db.go
│   └── models
│       └── user.go
└── storage
    └── migrations
        └── 20250701120306_create_users_table.sql

Кратко о каждом файле

main.go

Главный файл, где происходит всё взаимодействие с Telegram через tgbotapi, обработка команд (/start, /joined, /check, /me, /broadcast) и "разруливание" сценариев.
Здесь же используется godotenv для загрузки конфигов из .env.

package main

import (
	"fmt"
	"github.com/digkill/telegram_gateway_bot/internal/components/config"
	"github.com/digkill/telegram_gateway_bot/internal/components/crm"
	"github.com/digkill/telegram_gateway_bot/internal/components/db"
	"github.com/digkill/telegram_gateway_bot/internal/models"
	"github.com/joho/godotenv"
	"github.com/liudng/godump"
	"log"
	"os"
	"regexp"
	"time"

	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)

func main() {
	// Загружаем .env (опционально: если не найден — не ругается)
	_ = godotenv.Load()

	cfg := config.LoadConfig()

	conn, err := db.NewDB(cfg.MySQLDNS)
	if err != nil {
		log.Fatalf("DB connect error: %v", err)
	}
	if err := conn.Init(); err != nil {
		log.Fatalf("DB init error: %v", err)
	}

	bot, err := tgbotapi.NewBotAPI(cfg.TgToken)
	if err != nil {
		log.Panic(err)
	}

	u := tgbotapi.NewUpdate(0)
	u.Timeout = 60
	updates := bot.GetUpdatesChan(u)

	for update := range updates {
		if update.Message == nil {
			continue
		}
		chatID := update.Message.Chat.ID
		user := update.Message.From

		// Сохраняем пользователя, если новый
		conn.CreateUser(user.ID, user.UserName)
		urec, _ := conn.GetUser(user.ID)

		// --- Broadcast (только для админа)
		if update.Message.IsCommand() && update.Message.Command() == "broadcast" && user.UserName == cfg.AdminUser {
			text := update.Message.CommandArguments()
			users, _ := conn.AllUsers()
			for _, u := range users {
				bot.Send(tgbotapi.NewMessage(u.UserID, text))
				time.Sleep(50 * time.Millisecond)
			}
			bot.Send(tgbotapi.NewMessage(chatID, "Рассылка отправлена!"))
			continue
		}

		// --- /start
		if update.Message.IsCommand() && update.Message.Command() == "start" {
			welcome := `
<b>👋 Привет, %s!</b>

Рады видеть тебя в нашем Telegram-боте!
Здесь ты получаешь:
• 🎁 Спец-доступ к приватному каналу
• 📩 Уведомления о новостях и акциях
• 💬 Крутая поддержка

👇 Жми кнопку для вступления!
`
			msg := tgbotapi.NewMessage(chatID, fmt.Sprintf(welcome, user.FirstName))
			msg.ParseMode = "HTML"
			btn := tgbotapi.NewInlineKeyboardButtonURL("Вступить в канал", os.Getenv("INVITE_LINK"))
			keyboard := tgbotapi.NewInlineKeyboardMarkup(
				[]tgbotapi.InlineKeyboardButton{btn},
			)
			msg.ReplyMarkup = keyboard
			bot.Send(msg)
			conn.UpdateStatus(user.ID, "invited")
			return
		}

		// --- /joined (запуск регистрации)
		if update.Message.IsCommand() && update.Message.Command() == "joined" {
			conn.UpdateStatus(user.ID, "joined")
			conn.UpdateStep(user.ID, models.StepWaitEmail)
			bot.Send(tgbotapi.NewMessage(chatID, "Введи, пожалуйста, свой email:"))
			continue
		}

		// --- /check (проверка вступления в канал/группу)
		if update.Message.IsCommand() && update.Message.Command() == "check" {
			chatMember, err := bot.GetChatMember(tgbotapi.GetChatMemberConfig{
				ChatConfigWithUser: tgbotapi.ChatConfigWithUser{
					ChatID: cfg.ChannelID,
					UserID: user.ID,
				},
			})
			if err == nil && (chatMember.Status == "member" || chatMember.Status == "administrator" || chatMember.Status == "creator") {
				bot.Send(tgbotapi.NewMessage(chatID, "Ты уже в группе, красавчик!"))
				conn.UpdateStatus(user.ID, "joined")
			} else {
				bot.Send(tgbotapi.NewMessage(chatID, "Похоже, ты ещё не вступил в группу 😔"))
			}
			continue
		}

		// --- /me (инфа о себе)
		if update.Message.IsCommand() && update.Message.Command() == "me" {
			u, _ := conn.GetUser(user.ID)
			godump.Dump(u)
			msg := fmt.Sprintf("User: %s\nЗарегистрирован: %s\nСтатус: %s\nEmail: %s\nТелефон: %s\nUTM: %s",
				u.Username, u.FirstSeen, u.Status, u.Email, u.Phone, u.UTM)
			bot.Send(tgbotapi.NewMessage(chatID, msg))
			continue
		}

		// --- Registration steps
		step := urec.RegistrationStep
		if step == models.StepWaitEmail && !update.Message.IsCommand() {
			// Email validation
			email := update.Message.Text
			emailRegexp := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
			if !emailRegexp.MatchString(email) {
				bot.Send(tgbotapi.NewMessage(chatID, "Некорректный email, попробуй еще раз:"))
				continue
			}
			conn.UpdateEmail(user.ID, email)
			conn.UpdateStep(user.ID, models.StepWaitPhone)
			bot.Send(tgbotapi.NewMessage(chatID, "Спасибо! Теперь введи свой телефон:"))
			continue
		}
		if step == models.StepWaitPhone && !update.Message.IsCommand() {
			phone := update.Message.Text
			phoneRegexp := regexp.MustCompile(`^\+?\d[\d\s\-]{7,}$`)
			if !phoneRegexp.MatchString(phone) {
				bot.Send(tgbotapi.NewMessage(chatID, "Некорректный номер, попробуй еще раз:"))
				continue
			}
			conn.UpdatePhone(user.ID, phone)
			conn.UpdateStep(user.ID, models.StepDone)
			bot.Send(tgbotapi.NewMessage(chatID, "Спасибо! Ты успешно зарегистрирован как участник."))

			// Интеграция с CRM
			u2, _ := conn.GetUser(user.ID)
			go crm.SendToCRM(cfg.CRMEndpoint, *u2)
			continue
		}
	}
}

db.go

Обёртки над SQL-запросами к MySQL: добавление/обновление пользователя, получение статуса и шага регистрации, выгрузка всех лидов, обновление контактов и т.д. Всё вынесено в отдельные функции ради чистоты и переиспользуемости.

crm.go

Вся логика интеграции с внешними CRM или Google Sheets. Пример — отправка POST-запроса с инфой о лиде на внешний endpoint. При необходимости модифицируется под любую CRM (amoCRM, Bitrix24 и др.).

config.go

Модуль загрузки переменных окружения (os.Getenv) + парсинг .env через godotenv. Позволяет гибко настраивать бот без правок в коде.

types.go

Здесь объявлены все структуры пользователя (User), статусы и состояния регистрации (RegistrationStep), вспомогательные константы. Разделение типов позволяет быстро расширять и масштабировать проект.

migrate.go и /migrations

Миграции через goose — золотой стандарт для production. В /storage/migrations лежит SQL-файл, который создаёт таблицу users с нужными полями.
migrate.go позволяет запускать миграции программно.
Goose можно запускать и через CLI:

goose -dir ./migrations mysql "$MYSQL_DSN" up

.env

TG_TOKEN=ваш_токен_бота
MYSQL_DSN=логин:пароль@tcp(localhost:3306)/leadbot?parseTime=true
CHANNEL_ID=-1001234567890
ADMIN_USER=your_admin_username
CRM_ENDPOINT=https://your.crm/api/leads
INVITE_LINK=https://t.me/joinchat/xxxxxx

Файл не пушится в git (добавьте в .gitignore!).

Логика работы и фичи

Онбординг пользователя

  1. Пользователь пишет /start — получает приветствие с красивой кнопкой для вступления в канал.

  2. После вступления пишет /joined — бот спрашивает e-mail и телефон, валидирует и сохраняет в MySQL.

  3. По команде /check бот автоматом проверяет, состоит ли пользователь в канале (через GetChatMember).

  4. Все данные лидов можно выгрузить для рассылок или аналитики.

Админские фичи

  • Команда /broadcast <text> доступна только админу (ADMIN_USER). Позволяет разослать сообщение всем лидам из базы.

  • Возможна интеграция с любым webhook CRM — при завершении регистрации данные улетают на нужный endpoint.

Как развернуть проект

1. Склонировать репозиторий и сконфигурировать .env

git clone https://github.com/yourusername/telegram_gateway_bot.git
cd telegram_gateway_bot
# отредактируйте .env

2. Запустить миграции

go install github.com/pressly/goose/v3/cmd/goose@latest
goose -dir ./migrations mysql "$MYSQL_DSN" up

3. Собрать и запустить локально

go build -o telegram_gateway_bot
./telegram_gateway_bot

или

go run main.go

4. Настроить в продакшене как сервис

Создайте файл /etc/systemd/system/telegram_gateway_bot.service:

[Unit]
Description=Telegram Gateway Bot
After=network.target

[Service]
Type=simple
WorkingDirectory=/var/www/telegram_gateway_bot
ExecStart=/var/www/telegram_gateway_bot/telegram_gateway_bot
Restart=always
RestartSec=5
User=www-data
EnvironmentFile=/var/www/telegram_gateway_bot/.env

[Install]
WantedBy=multi-user.target

Затем:

sudo systemctl daemon-reload
sudo systemctl enable telegram_gateway_bot
sudo systemctl start telegram_gateway_bot
sudo systemctl status telegram_gateway_bot

Итог

Бот готов для использования как “ворота” в ваши приватные Telegram-сообщества, школы, мастер-майнды и любые лид-магниты.

Он легко кастомизируется, расширяется под любые цели, интегрируется с CRM и работает как сервис на сервере.

Вопросы, доработки, форки?

Пишите в комментарии или следите в Telegram канале за обновлениями и новостями: https://t.me/notecto

P.S.
Готовлю расширенную версию с платёжками, подпиской, доп. аналитикой, и автосегментацией лидов — следите за обновлениями!

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.