Сейчас почти на всех сайтах есть регистрация. Реализована она чаще всего с помощью e-mail, реже с помощью смс. А что если сделать регистрацию через telegram бота? В качестве логина на сайте мы сможем использовать подтверждённый номер телефона, а сам бот будет посылать одноразовые коды для входа. В данной статье описан процесс создания такого бота на языке Golang.

image
Пример работы бота

Хотелось бы сразу отметить: в статье не будет описана реализация входа на сайт по одноразовым кодам.

Весь код есть в репозитории на GitHub

Оглавление:


  1. Необходимое ПО
  2. Получение API ключа
  3. Структура проекта
  4. Файл настроек
  5. Инициализация бота и соединения с БД
  6. Основной код
  7. Запуск бота

Для начала нужно установить всё необходимое ПО:


Далее следует получить API ключ для нашего будущего бота. Для этого нужно написать боту BotFather. Что примерно должно получиться:

image

Перед тем как начать программировать нужно определится со структурой проекта, у меня она получилась такой:

/project/
	/conf/
		settings.go
	/src/
		database.go
		telegramBot.go
	main.go

Приступаем к написанию кода! Для начала файл настроек (settings.go):

const (
	TELEGRAM_BOT_API_KEY = "paste your key here"  // API  ключ, который мы получили у BotFather
	MONGODB_CONNECTION_URL = "localhost"  // Адрес сервера MongoDB
	MONGODB_DATABASE_NAME = "regbot"  // Название базы данных
	MONGODB_COLLECTION_USERS = "users"  // Название таблицы
)

Для каждого пользователя в БД хранятся: ид чата (chat_id) и номер мобильного телефона (phone_number). Поэтому сразу создаём структуру User:

type User struct {
	Chat_ID      int64
	Phone_Number string
}

Соединение с MongoDB реализуем с помощью структуры DatabaseConnection:

type DatabaseConnection struct {
	Session *mgo.Session  // Соединение с сервером
	DB      *mgo.Database // Соединение с базой данных
}

Бота представим в качестве структуры TelegramBot:

type TelegramBot struct {
	API                   *tgbotapi.BotAPI        // API телеграмма
	Updates               tgbotapi.UpdatesChannel // Канал обновлений
	ActiveContactRequests []int64                 // ID чатов, от которых мы ожидаем номер
}

Функция инициализации соединения с MongoDB:

func (connection *DatabaseConnection) Init() {
	session, err := mgo.Dial(conf.MONGODB_CONNECTION_URL)  // Подключение к серверу
	if err != nil {
		log.Fatal(err)  // При ошибке прерываем выполнение программы
	}
	connection.Session = session
	db := session.DB(conf.MONGODB_DATABASE_NAME)  // Подключение к базе данных
	connection.DB = db
}

Функция инициализации бота:

func (telegramBot *TelegramBot) Init() {
	botAPI, err := tgbotapi.NewBotAPI(conf.TELEGRAM_BOT_API_KEY)  // Инициализация API
	if err != nil {
		log.Fatal(err)
	}
	telegramBot.API = botAPI
	botUpdate := tgbotapi.NewUpdate(0)  // Инициализация канала обновлений
	botUpdate.Timeout = 64
	botUpdates, err := telegramBot.API.GetUpdatesChan(botUpdate)
	if err != nil {
		log.Fatal(err)
	}
	telegramBot.Updates = botUpdates
}

Бот инициализируется, но делать он ещё ничего не умеет. Давайте двигаться дальше!

Следующий шаг — «основной цикл бота»:

func (telegramBot *TelegramBot) Start() {
	for update := range telegramBot.Updates {
		if update.Message != nil {
			// Если сообщение есть  -> начинаем обработку
			telegramBot.analyzeUpdate(update)
		}
	}
}

Это бесконечный цикл. Обработка всех входящих сообщений начинается с него. В начале обработки мы должны проверить, есть ли пользователь у нас в базе. Нет — создаём и запрашиваем его номер, есть — продолжаем обработку.

// Начало обработки сообщения
func (telegramBot *TelegramBot) analyzeUpdate(update tgbotapi.Update) {
	chatID := update.Message.Chat.ID
	if telegramBot.findUser(chatID) {  // Есть ли пользователь в БД?
		telegramBot.analyzeUser(update)
	} else {
		telegramBot.createUser(User{chatID, ""})  // Создаём пользователя
		telegramBot.requestContact(chatID)  // Запрашиваем номер
	}
}

func (telegramBot *TelegramBot) findUser(chatID int64) bool {
	find, err := Connection.Find(chatID)
	if err != nil {
		msg := tgbotapi.NewMessage(chatID, "Произошла ошибка! Бот может работать неправильно!")
		telegramBot.API.Send(msg)
	}
	return find
}

func (telegramBot *TelegramBot) createUser(user User) {
	err := Connection.CreateUser(user)
	if err != nil {
		msg := tgbotapi.NewMessage(user.Chat_ID, "Произошла ошибка! Бот может работать неправильно!")
		telegramBot.API.Send(msg)
	}
}

func (telegramBot *TelegramBot) requestContact(chatID int64) {
        // Создаём сообщение
	requestContactMessage := tgbotapi.NewMessage(chatID, "Согласны ли вы предоставить ваш номер телефона для регистрации в системе?")
        // Создаём кнопку отправки контакта
	acceptButton := tgbotapi.NewKeyboardButtonContact("Да") 
	declineButton := tgbotapi.NewKeyboardButton("Нет")
        // Создаём клавиатуру
	requestContactReplyKeyboard := tgbotapi.NewReplyKeyboard([]tgbotapi.KeyboardButton{acceptButton, declineButton})
	requestContactMessage.ReplyMarkup = requestContactReplyKeyboard
	telegramBot.API.Send(requestContactMessage)  // Отправляем сообщение
	telegramBot.addContactRequestID(chatID)  // Добавляем ChatID в лист ожидания
}

func (telegramBot *TelegramBot) addContactRequestID(chatID int64) {
	telegramBot.ActiveContactRequests = append(telegramBot.ActiveContactRequests, chatID)
}

Начальная обработка написана, теперь давайте напишем общение с базой данных:

var Connection DatabaseConnection  // Переменная, через которую бот будет обращаться к БД

// Проверка на существование пользователя
func (connection *DatabaseConnection) Find(chatID int64) (bool, error) {
	collection := connection.DB.C(conf.MONGODB_COLLECTION_USERS)  // Получаем коллекцию "users"
	count, err := collection.Find(bson.M{"chat_id": chatID}).Count()  // Считаем количество записей с заданным ChatID
	if err != nil || count == 0 { 
		return false, err
	} else {
		return true, err
	}
}

// Получение пользователя
func (connection *DatabaseConnection) GetUser(chatID int64) (User, error) {
	var result User
	find, err := connection.Find(chatID)  // Сначала проверяем, существует ли он
	if err != nil {
		return result, err
	}
	if find {  // Если да -> получаем
		collection := connection.DB.C(conf.MONGODB_COLLECTION_USERS)  
		err = collection.Find(bson.M{"chat_id": chatID}).One(&result)
		return result, err
	} else {  // Нет -> возвращаем NotFound
		return result, mgo.ErrNotFound
	}
}

// Создание пользователя
func (connection *DatabaseConnection) CreateUser(user User) error {
	collection := connection.DB.C(conf.MONGODB_COLLECTION_USERS)
	err := collection.Insert(user)
	return err
}

// Обновление номера мобильного телефона
func (connection *DatabaseConnection) UpdateUser(user User) error {
	collection := connection.DB.C(conf.MONGODB_COLLECTION_USERS)
	err := collection.Update(bson.M{"chat_id": user.Chat_ID}, &user)
	return err
}

Это все функции, которые нам пригодятся. Для бота осталось написать функции продолжения обработки сообщения и анализа контакта:

func (telegramBot *TelegramBot) analyzeUser(update tgbotapi.Update) {
	chatID := update.Message.Chat.ID
	user, err := Connection.GetUser(chatID)  // Вытаскиваем данные из БД для проверки номера
	if err != nil {
		msg := tgbotapi.NewMessage(chatID, "Произошла ошибка! Бот может работать неправильно!")
		telegramBot.API.Send(msg)
		return
	}
	if len(user.Phone_Number) > 0 {
		msg := tgbotapi.NewMessage(chatID, "Ваш номер: " + user.Phone_Number)  // Если номер у нас уже есть, то пишем его
		telegramBot.API.Send(msg)
		return
	} else {
		// Если номера нет, то проверяем ждём ли мы контакт от этого ChatID
		if telegramBot.findContactRequestID(chatID) {
			telegramBot.checkRequestContactReply(update)  // Если да -> проверяем
			return
		} else {
			telegramBot.requestContact(chatID)  // Если нет -> запрашиваем его
			return
		}
	}
}

// Проверка принятого контакта
func (telegramBot *TelegramBot) checkRequestContactReply(update tgbotapi.Update) {
	if update.Message.Contact != nil {  // Проверяем, содержит ли сообщение контакт
		if update.Message.Contact.UserID == update.Message.From.ID {  // Проверяем действительно ли это контакт отправителя
			telegramBot.updateUser(User{update.Message.Chat.ID, update.Message.Contact.PhoneNumber}, update.Message.Chat.ID)  // Обновляем номер
			telegramBot.deleteContactRequestID(update.Message.Chat.ID)  // Удаляем ChatID из списка ожидания
			msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Спасибо!")
			msg.ReplyMarkup = tgbotapi.NewRemoveKeyboard(false)  // Убираем клавиатуру
			telegramBot.API.Send(msg)
		} else {
			msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Номер телефона, который вы предоставили, принадлежит не вам!")
			telegramBot.API.Send(msg)
			telegramBot.requestContact(update.Message.Chat.ID)
		}
	} else {
		msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Если вы не предоставите ваш номер телефона, вы не сможете пользоваться системой!")
		telegramBot.API.Send(msg)
		telegramBot.requestContact(update.Message.Chat.ID)
	}
}

// Обновление номера мобильного телефона пользователя
func (telegramBot *TelegramBot) updateUser(user User, chatID int64) {
	err := Connection.UpdateUser(user)
	if err != nil {
		msg := tgbotapi.NewMessage(chatID, "Произошла ошибка! Бот может работать неправильно!")
		telegramBot.API.Send(msg)
		return
	}
}

// Есть ChatID в листе ожидания?
func (telegramBot *TelegramBot) findContactRequestID(chatID int64) bool {
	for _, v := range telegramBot.ActiveContactRequests {
		if v == chatID {
			return true
		}
	}
	return false
}

// Удаление ChatID из листа ожидания
func (telegramBot *TelegramBot) deleteContactRequestID(chatID int64) {
	for i, v := range telegramBot.ActiveContactRequests {
		if v == chatID {
			copy(telegramBot.ActiveContactRequests[i:], telegramBot.ActiveContactRequests[i + 1:])
			telegramBot.ActiveContactRequests[len(telegramBot.ActiveContactRequests) - 1] = 0
			telegramBot.ActiveContactRequests = telegramBot.ActiveContactRequests[:len(telegramBot.ActiveContactRequests) - 1]
		}
	}
}

Наш бот готов к работе! Осталось только его запустить. Для этого в main.go пишем:

var telegramBot src.TelegramBot

func main() {
	src.Connection.Init()  // Инициализация соединения с БД
	telegramBot.Init()  // Инициализация бота
	telegramBot.Start() 
}

В итоге у нас получился бот, запрашивающий у пользователя его номер, который в дальнейшем будет использоваться в системе. Бота можно и нужно улучшить, добавить интеграцию с сайтом, вход по одноразовым паролям, и.т.д.

Несомненно у данной системы есть минусы, самый очевидный из которых: зависимость от Telegram (изменение API, блокировка). По мне это хорошая альтернатива e-mail и sms, будете ли вы использовать её — решать вам. Спасибо за внимание!
Поделиться с друзьями
-->

Комментарии (21)


  1. frees2
    27.06.2017 06:59

    «Если вы не предоставите ваш номер телефона, вы не сможете пользоваться системой!»

    Для чего всё это надо?!


    1. De1aY
      27.06.2017 07:18

      Когда мы запрашиваем у пользователя номер, мы ожидаем, что следующее его сообщение будет контактом. Но он может отправить просто какой-нибудь текст (например: привет). В этом случае ответ бота будет таким.


    1. dmxrand
      27.06.2017 20:41
      +1

      Они хотят лишиться части (большей) пользователей.


  1. maxpy
    27.06.2017 07:18

    Данный бот уже запущен и работает где нибудь в вашей системе? Или просто как идею подаете?


    1. De1aY
      27.06.2017 07:24

      Да, в моём проект данный бот сейчас работает. Он используется для регистрации пользователей и отправки им уведомлений. Проект в альфе.


      1. maxpy
        27.06.2017 08:37

        Проект доступен с Интернета и обычным пользователям? Можно поиграться?


        1. De1aY
          27.06.2017 23:54

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


  1. DeLuxis
    27.06.2017 07:51
    +1

    Из-за дорогих СМС у нас тоже перешли на телеграм бота. Писали на Java.


  1. Akuma
    27.06.2017 08:47

    Идея хорошая, а если СМС выходит дорого, то еще и выгодная.
    Но если используете только для регистрации… ну неужто у вас так много регистраций, что СМС на них не окупается?

    Конечно зависит от аудитории, но если у вас не-ИТ сайт, то такая регистрация будет отсеивать неплохую часть пользователей. Да, не все пользуются Телеграммом.

    Собирали статистику, как много людей эта регистрация отпугивает? На какой тематике проекта? Пощелкать можно? :)


  1. frees2
    27.06.2017 09:31

    Можно ли вычислить «игру»?
    Наверное, да. Когда возникают некоторые закономерности в логике.
    С телеграммом идёт давняя «игра с привязкой к телефону».
    Кто её ведёт, црушнеГи, кгбешнеГи, сам Дуров?


    1. arvitaly
      27.06.2017 17:06

      Защита от ботов?


      1. frees2
        27.06.2017 18:36

        $(document).ready(function() { $('#btn1666').hover(function(){ $(this).remove();
        $.getJSON('
        

        Самое примитивное, сам когда то придумал. Имитация мыши у бота есть? Не убрал мышку с экрана, появляется нужная информация, к примеру для регистрации.


  1. mihmig
    27.06.2017 09:54
    +2

    А зачем я должен указывать на вашем сайте свой телефон?
    Дайте имя бота (например @%ProjectName%_RegBot)
    Я через него зарегистрируюсь (запрошу код подтверждения) и всё.


  1. x07
    27.06.2017 10:04

    Описывал не так давно нечто подобное, как раз вход на сайт и регистрация автоматом. https://habrahabr.ru/post/321682/ Как видно из комментариев, не так много поддерживают такую идею


    1. gudvinr
      27.06.2017 12:28

      Возможно, из-за излишней сложности. У вас — пароли, тут телефон запрашивают.
      У телеграма есть deep linking и на сайте для регистрации/аутентификации можно просто давать ссылки с одноразовыми токенами, которые обрабатывать ботом.
      От OAUTH, в принципе, мало чем отличается.


      Сам номер телефона, конечно, может быть необходим для подтверждения личности при заказе в онлайн-магазинах, например, но для регистрации в целом — нет.


  1. DorianPeregrim
    27.06.2017 13:10

    А что за странное именование файла с логикой бота и полей структуры пользователя? И почему не захотели делать все в одном пакете, учитывая, что всего 4 файла в проекте?


  1. astec
    27.06.2017 15:22

    Делал тоже самое и тоже на Go для связи моего бота по учёту долгов @DebtsTrackerRuBot с веб-версией и приложением для мобилок https://debtstracker.io/ru/ — альфа, но можно потыкать.

    Кстати, если кому интересно делаю фреймворк на Go для ботов под разные платформы: https://github.com/strongo/bots-framework.

    Пока только Telegram использую, но планирую добавить Viber, WeChat & FB Messenger в ближайшее время. Пулл реквесты приветствуются.


  1. reinvent
    27.06.2017 15:58

    Можно ли сделать бота, который будет за пользователя проходить регистрацию по «старому» формату, вписывая все данные, которые предоставил ему пользователь, останавливаясь только на месте, где именно человек должен ввести информацию?
    Автоматизировать то, что можно автоматизировать при регистрации.


  1. Jokerjar
    27.06.2017 16:29

    Вместо цикла, конечно удобнее использовать WebHook. И номер телефона, имхо — лишняя штука здесь. Лучше использовать внутренний идентификатор юзера в Телеграме.


  1. dikkini
    28.06.2017 00:49

    Почему Go?


    1. De1aY
      28.06.2017 00:52
      -1

      Всё просто — я знаком с ним лучше, чем с другими языками