Создание Twitter-бота в Go
Это короткое пошаговое руководство по созданию твиттер-бота. Бот, который я создам в этом уроке, будет называться «Big Pineapple Guy», и его цель будет заключаться в том, чтобы комментировать сообщения моего друга с фактами об ананасах. Я собираюсь сделать бота на Go и развернуть его на Heroku, чтобы все было бесплатно. Конечным результатом этого будет бот, который комментирует такие вещи:
Весь код для этого можно найти здесь
Получение учетных данных Twitter
Я хочу, чтобы у моего бота была собственная учетная запись Twitter, поэтому первый шаг для всего этого - перейти в Twitter и создать новую учетную запись для бота.
Затем мне нужен доступ к этой учетной записи по api. Для этого мне нужно зарегистрироваться в качестве разработчика на https://developer.twitter.com. Оттуда я создаю новое приложение и заполняю его основной информацией о том, что будет делать бот.
Как только приложение создано, я могу пойти и получить нужные мне ключи и токены.
Загрузка учетных данных
Я собираюсь взять ключи и токены, которые дал мне Twitter, и поместить их в файл yaml, чтобы получить к ним доступ во время разработки.
ConsumerKey: YOUR_CONSUMER_KEY
ConsumerSecret: YOUR_CONSUMER_SECRET
AccessToken: YOUR_ACCESS_TOKEN
AccessSecret: YOUR_ACCESS_SECRET
Для первой части кода go я собираюсь написать что-нибудь, чтобы получить эти учетные данные из файла yaml. Есть библиотека для разбора yaml, которая мне понадобится в первую очередь.
~$ go mod init // initializing a go module to keep track of dependencies
~$ go get gopkg.in/yaml.v2
Теперь, когда у меня есть библиотека, я могу написать базовую функцию для получения учетных данных. Я решил поместить его в отдельный файл, чтобы все было в чистоте.
package main
import (
"fmt"
"os"
"log"
"io/ioutil"
"gopkg.in/yaml.v2"
)
const (
credsFile = "creds.yml"
)
type creds struct {
ConsumerKey string `yaml:"ConsumerKey"`
ConsumerSecret string `yaml:"ConsumerSecret"`
AccessToken string `yaml:"AccessToken"`
AccessSecret string `yaml:"AccessSecret"`
}
func getCreds() *creds {
c := &creds{}
// Read File
yamlFile, err := ioutil.ReadFile(credsFile)
if err != nil {
log.Fatalf("[ERROR] %v", err)
}
// Unmarshall Creds
err = yaml.Unmarshal(yamlFile, c)
if err != nil {
log.Fatalf("[ERROR] %v", err)
}
return c
}
Пока каталог для проекта должен выглядеть так:
.
├── creds.go
└── creds.yml
Добавление основного кода
Перед программированием main мне нужно найти хорошую клиентскую библиотеку twitter API. К счастью, похоже, есть действительно интересный вариант от пользователя Github по имени dghubble.
go get github.com/dghubble/go-twitter/twitter
Я хочу, чтобы мой бот прислушивался к новым твитам от определенных дескрипторов твиттера и запускал какой-то код всякий раз, когда это происходит. Это можно сделать, прослушивая потоки событий Twitter. Twitter дает разработчикам возможность отслеживать события и отфильтровывать нежелательные события. Документация по нему находится здесь. В моем случае я хочу фильтровать по конкретным дескрипторам твиттера. В библиотеке go twitter, которую я использую, есть пример того, как слушать потоки twitter (https://developer.twitter.com/en/docs/tweets/filter-realtime/overview). Исходя из этого примера, я могу создать функцию main для своего бота.
package main
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/dghubble/oauth1"
"github.com/dghubble/go-twitter/twitter"
)
func main() {
creds := getCreds()
if creds.ConsumerKey == "" || creds.ConsumerSecret == "" || creds.AccessToken == "" || creds.AccessSecret == "" {
log.Fatal("Consumer key/secret and Access token/secret required")
}
config := oauth1.NewConfig(creds.ConsumerKey, creds.ConsumerSecret)
token := oauth1.NewToken(creds.AccessToken, creds.AccessSecret)
// OAuth1 http.Client will automatically authorize Requests
httpClient := config.Client(oauth1.NoContext, token)
// Twitter Client
client := twitter.NewClient(httpClient)
// Convenience Demux demultiplexed stream messages
demux := twitter.NewSwitchDemux()
demux.Tweet = func(tweet *twitter.Tweet) {
fmt.Println("[INFO] ", tweet.Text)
}
demux.DM = func(dm *twitter.DirectMessage) {
fmt.Println("[INFO] DM: ", dm.SenderID)
}
demux.Event = func(event *twitter.Event) {
fmt.Printf("[INFO] Event: %#v\n", event)
}
fmt.Println("Starting Stream...")
// FILTER
filterParams := &twitter.StreamFilterParams{
Track: []string{"cat"},
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.Filter(filterParams)
if err != nil {
log.Fatal(err)
}
// Receive messages until stopped or stream quits
go demux.HandleChan(stream.Messages)
// Wait for SIGINT and SIGTERM (HIT CTRL-C)
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
log.Println(<-ch)
fmt.Println("Stopping Stream...")
stream.Stop()
}
demux
это то, что используется для разделения различных типов сообщений в потоке. Для каждого типа сообщения (например, твита, DM, события) я регистрирую обратный вызов, который вызывается каждый раз, когда происходит одно из этих событий. Пока я просто распечатываю сообщения, когда получаю их.
Над функцией main
я собираюсь поместить переменную, которая представляет собой массив всех дескрипторов Twitter, которые я хочу отфильтровать.
var victims = []string{
"A_Chris_Kahuna",
"georgiopizzeria",
"HakaRoland",
}
func main() {
...
К сожалению, я не могу фильтровать только по идентификаторам Twitter. Поэтому мне нужно написать что-то, что преобразует массив дескрипторов Twitter в массив идентификаторов Twitter.
import (
...
"strconv"
...
)
...
// Twitter Client
client := twitter.NewClient(httpClient)
var ids = []string{}
for _, name := range victims {
// User Show
user, _, err := client.Users.Show(&twitter.UserShowParams{
ScreenName: name,
})
if err != nil {
fmt.Println("[ERROR] on ", name)
continue
}
ids = append(ids, strconv.FormatInt(user.ID, 10))
}
// Convenience Demux demultiplexed stream messages
demux := twitter.NewSwitchDemux()
...
Используя этот новый массив ids
, я могу фильтровать поток.
...
fmt.Println("Starting Stream...")
// FILTER
filterParams := &twitter.StreamFilterParams{
Follow: ids,
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.Filter(filterParams)
if err != nil {
log.Fatal(err)
}
// Receive messages until stopped or stream quits
go demux.HandleChan(stream.Messages)
...
Теперь, когда я выполняю фильтрацию по идентификаторам Twitter и получаю только те твиты, которые мне нужны, я могу начать писать логику того, что делать, когда я получаю новое твит-сообщение.
demux.Tweet = func(tweet *twitter.Tweet) {
// Ignore RTs
if tweet.Retweeted {
return
}
// Ignore Replies
if tweet.InReplyToStatusID != 0 || tweet.InReplyToScreenName != "" || tweet.InReplyToUserIDStr != "" {
return
}
fmt.Println("[INFO] Tweet: ", tweet.Text)
}
В Twitter API у твитов много полей. В используемой мной библиотеке этот файл содержит структуру твита со всеми доступными полями. Я хочу взаимодействовать только с твитами, отправленными прямо из моих полей, без ретвитов или ответов, поэтому это было первое, чего я стараюсь избегать. Благодаря этому у меня есть все необходимое, чтобы перейти к интересной части.
Собираем все вместе
Теперь я должен решить, чем я хочу ответить. Я хочу, чтобы ответом был случайный факт об ананасе, поэтому, чтобы все было в порядке, я собираюсь поместить список фактов в новый файл.
package main
var facts = [...]string{
"Pineapple can reach 3.3 to 4.9 feet in height. Large specimens of this fruit can reach nearly 20 pounds of weight.",
"Pineapple is perennial herbaceous plant that has short and stocky stem. Its leaves are spiny and covered with wax on the surface.",
"Fruit of pineapple is result of fusion of 100 to 200 individual flowers.",
"Color of the fruit depends on the variety. Pineapples are usually red, purple or lavender in color.",
"Christopher Columbus brought pineapple from South America to Europe. This fruit was first named \"pina\" because it looks like large pine cone. Englishmen later added name \"apple\" to denote it as a fruit.",
"Two types of pineapples, called \"cayenne pineapple\" and \"red Spanish pineapple\" are currently cultivated and consumed throughout the world. They differ in color and size of the fruit.",
"Birds (such as hummingbirds) and bats pollinate pineapples.",
"One plant produces only one pineapple per season. Ripening process ends after removal of the fruit from the stem.",
"Pineapple is rich source of fibers, manganese, vitamin C and vitamins of the B group. It can be consumed raw, in the form of juices, or as a part of various sweet and salty dishes. Pina colada is popular drink that is made of pineapples.",
"Pineapple is used to alleviate symptoms of common cold, cough and nasal congestion (by decreasing the amount of mucus). It can reduce inflammation and prevent development of blood clots.",
"Pineapple contains bromelain, a substance which decomposes proteins and facilitates digestion. People can use marinade made of pineapple juice to reduce toughness of the meat (bromelain acts as softener of meat).",
"Pineapple can be easily cultivated using the crown of the fruit. Crown should be cut and partially dried before planting in the soil.",
"South Asia is the biggest producer of pineapples. More than 2 million tons of pineapples are produced and exported from Philippines each year.",
"Almost all parts of the pineapple can be used in the production of vinegar and alcohol. Inedible parts can be used as food for domestic animals.",
"Pineapple can live and produce fruit for up to 50 years in the wild.",
"Pineapples regenerate! You can plant pineapple leaves to grow a new plant.",
"Hawaii produces about 1/3 of all pineapples in the world.",
"Pineapples are a cluster of hundreds of fruitlets.",
"Pineapples take about 18-20 months to become ready to harvest.",
"Pineapple is the only edible fruit of its kind, the Bromeliads.",
"One pineapple plant can produce one pineapple at a time.",
"Pineapples ripen faster upside down.",
"A pineapple is not an apple, or pine. It’s actually a berry!",
"The world’s largest pineapple ever recorded was in 2011, grown by Christine McCallum from Bakewell, Australia. It measured 32cm long, 66cm girth and weighed a whopping 8.28kg!",
"The Hawaiian word for pineapple is ‘halakahiki‘.",
"The Dole Plantation’s Pineapple Garden Maze in Hawaii has the record for the largest maze in the world, stretching over three acres!",
}
Используя генератор случайных чисел в стандартной библиотеке Go, я собираюсь выбрать случайный элемент из списка, чтобы ответить. Очень важно заполнить генератор чисел текущим временем, потому что я собираюсь развернуть его позже. Это просто гарантирует, что я действительно использую случайное число.
import (
...
"math/rand"
"time"
...
)
...
// Twitter Client
client := twitter.NewClient(httpClient)
rand.Seed(time.Now().UnixNano()) // Seed the random number generator
var ids = []string{}
...
// Ignore Replies
if tweet.InReplyToStatusID != 0 || tweet.InReplyToScreenName != "" || tweet.InReplyToUserIDStr != "" {
return
}
choice := facts[rand.Intn(len(facts))] // Pick a random fact
botResponse := fmt.Sprintf("@%s Pineapple Fact: %s", tweet.User.ScreenName, choice)
fmt.Println("[INFO] Tweet: ", tweet.Text)
...
Наконец, я собираюсь добавить логику ответа на твит.
demux.Tweet = func(tweet *twitter.Tweet) {
...
// Reply to Tweet
reply, _, err := client.Statuses.Update(
botResponse,
&twitter.StatusUpdateParams{
InReplyToStatusID: tweet.ID,
},
)
if err != nil {
fmt.Println(err)
}
fmt.Println("[INFO] ", reply)
}
Если все настроено правильно и ошибок нет, я смогу собрать и запустить это на моей собственной машине.
~$ go build
~$ ./BigPineappleGuy
Чтобы проверить, я добавил свою личную учетную запись в Твиттере к множеству жертв и написал несколько тестовых твитов. Когда все работает, я могу перейти к развертыванию бота, поэтому мне не нужно запускать его на моем собственном компьютере.
Развертывание в Heroku
Прежде чем что-либо делать в Heroku, мне нужно внести некоторые изменения в то, как я запускал бот. Во-первых, как я получаю учетные данные. Я действительно не хочу проверять свои учетные данные в git и отправлять их в Github, поэтому я собираюсь изменить файл cred.go
, чтобы иметь возможность принимать учетные данные из переменных среды.
func getCreds() *creds {
c := &creds{}
// Read File
yamlFile, err := ioutil.ReadFile(credsFile)
if err != nil {
fmt.Println("[INFO] No yml config, pulling from environment")
c = &creds{
ConsumerKey: os.Getenv("CONSUMER_KEY"),
ConsumerSecret: os.Getenv("CONSUMER_SECRET"),
AccessToken: os.Getenv("ACCESS_TOKEN"),
AccessSecret: os.Getenv("ACCESS_SECRET"),
}
} else {
// Unmarshall Creds
err = yaml.Unmarshal(yamlFile, c)
if err != nil {
log.Fatalf("[ERROR] %v", err)
}
}
return c
}
После этой модификации я могу войти в свою учетную запись Heroku и создать новый проект. В этом новом проекте я собираюсь ввести все свои ключи и токены как переменные среды через веб-консоль.
Однако перед развертыванием я добавлю Procfile
в папку своего проекта. Это скажет Heroku, что мой проект на самом деле не веб-сайт, а просто бот.
worker: BigPineappleGuy
На данный момент папка моего проекта выглядит так:
.
├── Procfile
├── creds.go
├── facts.go
├── go.mod
├── go.sum
└── main.go
Теперь я готов разместить все свои материалы на Github и указать Heroku в моем репозитории Github для развертывания.
И с этим я закончил! У меня есть твиттер-бот, написанный на Heroku. Это был действительно забавный проект, и было действительно приятно видеть, как мои друзья публикуют в нашем групповом чате фотографии таинственного ананасового аккаунта, комментирующего свои сообщения.