Боты на Golang для Telegram, VK и Discord

Достаточно долгое время я писал всех своих ботов на Node.js. Мне хватало его возможностей, однако со временем я зашел в тупик, потому что определенные модули работали очень медленно. Попытки оптимизировать не закончились каким-то серьезным успехом, поэтому я решил переписать одного из своих ботов полностью на Go.

Со временем, изучив основную механику работы ботов на Go я понял, что тут можно очень просто написать кросс-платформенных ботов. То есть не только для Telegram, но, например, также для VK и Discord.

Этот пост не является гайдом по написанию ботов на Go и призван указать лишь на простоту реализации данной идеи.

Для работы с API платформ я использую три библиотеки:

  1. Telegram
  2. VK
  3. Discord

Все эти библиотеки в разной степени похожи друг на друга, что делает портирование очень простым процессом. Рассмотрим примеры:

Код для Telegram:

func StartCommand(update tgbotapi.Update) {
    user, err := GetUser(update.Message.From.ID)
    if err != nil {
        log.Fatal(err)
        return
    }
    msg := tgbotapi.NewMessage(update.Message.Chat.ID, fmt.Sprintf("Hello, %s!", user.Name))
    bot.Send(msg)
}

Код для VK:

func StartCommand(update vkapi.LPUpdate) {
    user, err := GetUser(update.Message.From.ID)
    if err != nil {
        log.Fatal(err)
        return
    }
    
    msg := vkapi.NewMessage(vkapi.NewDstFromUserID(update.Message.FromID), fmt.Sprintf("Hello, %s!", user.Name))
    client.SendMessage(msg)
}

Код для Discord:

func StartCommand(m *discordgo.MessageCreate) {
    user, err := GetUser(update.Message.From.ID)
    if err != nil {
        log.Fatal(err)
        return
    }
    
	dg.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Hello, %s!", user.Name))
}

Из этого примера отлично видно, что вся логика команды одинаковая, отличен только механизм отправки ответа. Сама логика получения обновлений также очень похожа, рассмотрим рабочие примеры:

Код для Telegram:

var (
	bot *tgbotapi.BotAPI
)

func main() {
	var err error
	// Get token from environment
	token := os.Getenv("TOKEN")
	if token == "" {
		log.Fatal("TOKEN env variable not specified!")
	}
	// Init new bot
	bot, err = tgbotapi.NewBotAPI(token)
	if err != nil {
		log.Fatal(err)
	}
	
	u := tgbotapi.NewUpdate(0)
	u.Timeout = 60
	updates, err := bot.GetUpdatesChan(u)
	for update := range updates {
		// Skip, if it's not text message
		if update.Message == nil {
			continue
		}
		if strings.HasPrefix(update.Message.Text, "/start") {
			go StartCommand(update)
		}
		
		// Here you can make other commands
	}
}

Код для VK:

var (
	client *vkapi.Client
)

func main() {
	var err error
	
	// Get token from environment
	token := os.Getenv("TOKEN")
	if token == "" {
		log.Fatal("TOKEN env variable not specified!")
	}
	
	client, err = vkapi.NewClientFromToken(token)
	if err != nil {
		log.Fatal(err)
	}
	
	if err := client.InitLongPoll(0, 2); err != nil {
		log.Fatal(err)
	}
	
	updates, _, err := client.GetLPUpdatesChan(100, vkapi.LPConfig{25, vkapi.LPModeAttachments})
	if err != nil {
		log.Fatal(err)
	}
	
	for update := range updates {
		// Ignore all messages created by the bot itself
		if update.Message == nil || !update.IsNewMessage() || update.Message.Outbox() {
			continue
		}
		
		// VK specific
		command := strings.ToLower(update.Message.Text)
		if strings.HasPrefix(command, "start") {
			commandLogger.Info("command start triggered")
			go StartCommand(update)
		}
		
		// Here you can make other commands
	}
}

Код для Discord:

var (
	dg *discordgo.Session
)

func main() {
	var err error
	
	// Get token from environment
	token := os.Getenv("TOKEN")
	if token == "" {
		log.Fatal("TOKEN env variable not specified!")
	}
	
	// Create a new Discord session using the provided bot token
	dg, err = discordgo.New("Bot " + token)
	if err != nil {
		log.Fatal("error creating Discord session, ", err)
	}
	
	// Register the messageCreate func as a callback for MessageCreate events
	dg.AddHandler(messageCreate)
	
	// Open a websocket connection to Discord and begin listening.
	err = dg.Open()
	if err != nil {
		log.Fatal("error opening connection, ", err)
	}
	
	// Cleanly close down the Discord session.
	defer dg.Close()
}

// This function will be called (due to AddHandler above) every time a new message is created on any channel that the autenticated bot has access to
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
	// Ignore all messages created by the bot itself
	if m.Author.ID == s.State.User.ID {
		return
	}

	if strings.HasPrefix(m.Content, "/start") {
		go StartCommand(m)
	}
	
	// Here you can make other commands
}

Cложности вызывает только постоянный процесс бекпортирования изменений. Сначала я пишу версию для Telegram, а потом переношу изменения в версии для VK и Discord. Со временем это надоедает, поэтому стараюсь бекпортировать сразу крупные релизы. В перспективе надо бы написать генератор, который на базе кода Telegram-версии, генерировал исходники для VK- и Discord-версий.