+3

Dễ Dàng Tạo Bot Subscribe Twitter Với Golang

Chẳng là đợt này mình đang định làm một con bot follow các trang tin nối tiếng trên Twitter. Nên cũng có vọc vạch tìm hiểu các API về thằng Twitter này tiện thể làm quen với Golang luôn. Thì hôm nay mình sẽ xin được chia sẻ với mọi người những gì mình mới tìm hiểu được...Let's go

Giới Thiệu

Sau khi đọc qua đống API của Twitter thì mình đã tìm ra cách để follow real-time các trang tin mong muốn. Đó là Twitter có cung cấp cho chúng ta một cái gọi là Stream để ta có thể subscribe liên tục và có thể set điều kiện filter cho thằng stream này. Mình sẽ thử demo nhẹ một cái stream thả cửa như sau 😄 😄 😄

Đây là khi chúng ta không đặt điều kiện filter gì cho connect stream, nên nó sẽ get tất cả các tweet, retweet, quote public mà nó nghe được.

Khi tìm hiểu về các ví dụ và các tutorial trong docs của Twitter thì mình cũng tìm thấy một thư viện khá hay và tiện dụng được viết bằng golang. Nó hỗ trợ rất nhiều trong API của Twitter sẽ giúp chúng ta tiết kiếm được thời gian phát triển. Đó go-twitter do một tech lead của Twitter phát triển Dalton Hubble, thư viện thì hỗ trợ rất nhiều tính năng như sau

Trên đây là phần giới thiệu qua về thư viện mà chúng ta sẽ sử dụng và bây giờ hãy cùng bắt tay xây dựng con bot nha

Triển khai

Trước khi có thể thực hiện được các tác vụ với Twitter thì bạn cần phải tạo một tài khoản developer trên Twitter bằng cách truy cập đường link sau https://developer.twitter.com/en/apply-for-access và thực hiện các bước như bên dưới :

Xong bước này rồi thì phải ăn xôi đứng chờ bên Twitter họ duyệt cho thôi, à tầm mấy hôm mới được duyệt đấy nên anh cứ vừa ăn xôi vừa thư giãn thoải mái mà đợi 😅😅😅

 

Khởi tạo project

Nếu bạn nào đã có tài khoản developer trước đó rồi thì ta đi vào triển thôi. Đầu tiên là khởi tạo một project golang tạo một thư mục bot-twitter sau đó khởi tạo một module path và tạo một file main.go

   go mod init nghia/botnews
// main.go

   package main

   import "fmt"

   func main() {
       fmt.Println("Hello, world.")
   }

Test thử kết quả :

 

Thiết lập Stream

Bây giờ cần cài đặt thư viện để sử dụng

    go get github.com/dghubble/go-twitter/twitter

Setup stream giống với example của go-twitter

    package main

    import (
        "flag"
        "fmt"
        "log"
        "os"
        "os/signal"
        "syscall"

        "github.com/coreos/pkg/flagutil"
        "github.com/dghubble/go-twitter/twitter"
        "github.com/dghubble/oauth1"
        "github.com/joho/godotenv"
    )

    func main() {

        //Load env
        err := godotenv.Load()
        if err != nil {
            log.Fatal("Error loading .env file")
            fmt.Println("Error loading .env file")
        }

        flags := flag.NewFlagSet("user-auth", flag.ExitOnError)
        consumerKey := flags.String("consumer-key", os.Getenv("CONSUMER_KEY"), "Twitter Consumer Key")
        consumerSecret := flags.String("consumer-secret", os.Getenv("CONSUMER_SECRET"), "Twitter Consumer Secret")
        accessToken := flags.String("access-token", os.Getenv("ACCESS_TOKEN_KEY"), "Twitter Access Token")
        accessSecret := flags.String("access-secret", os.Getenv("ACCESS_TOKEN_SECRET"), "Twitter Access Secret")
        flags.Parse(os.Args[1:])
        flagutil.SetFlagsFromEnv(flags, "TWITTER")

        if *consumerKey == "" || *consumerSecret == "" || *accessToken == "" || *accessSecret == "" {
            log.Fatal("Consumer key/secret and Access token/secret required")
        }

        config := oauth1.NewConfig(*consumerKey, *consumerSecret)
        token := oauth1.NewToken(*accessToken, *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("+-----------------------------------------------------------------------------------+")
            fmt.Println("| ")
            fmt.Println(tweet.Text)
            fmt.Println("| ")
            fmt.Println("+-----------------------------------------------------------------------------------+")
            fmt.Println("\n")
        }
        demux.DM = func(dm *twitter.DirectMessage) {
            fmt.Println(dm.SenderID)
        }
        demux.Event = func(event *twitter.Event) {
            fmt.Printf("%#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)
        }

        // USER (quick test: auth'd user likes a tweet -> event)
        // userParams := &twitter.StreamUserParams{
        // 	StallWarnings: twitter.Bool(true),
        // 	With:          "followings",
        // 	Language:      []string{"en"},
        // }
        // stream, err := client.Streams.User(userParams)
        // if err != nil {
        // 	log.Fatal(err)
        // }

        // SAMPLE
        // sampleParams := &twitter.StreamSampleParams{
        // 	StallWarnings: twitter.Bool(true),
        // }
        // stream, err := client.Streams.Sample(sampleParams)
        // 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()
    }

Đặt các biến môi trường

// .env

CONSUMER_KEY= <App Key === API Key === Consumer API Key === Consumer Key === Customer Key === oauth_consumer_key>
CONSUMER_SECRET= <App Key Secret === API Secret Key === Consumer Secret === Consumer Key === Customer Key === oauth_consumer_secret>
ACCESS_TOKEN_KEY= <Access token === Token === resulting oauth_token>
ACCESS_TOKEN_SECRET= <Access token secret === Token Secret === resulting oauth_token_secret>
BEARER_TOKEN= < $BEARER_TOKEN > 

Chạy thử cái coi sao

ok chạy rồi dù chưa có bộ lọc nên chạy hơi tung tóe

 

Tạo bộ lọc

Phần này chúng ta sẽ tìm hiểu một chút về bộ lọc của stream. Đầu tiên ta sẽ thử xem bộ lọc StreamFilterParams xem sao nha

Nó có các tham số như FilterLevel, Follow, Language, Locations, StallWarnings, Track thì ta sẽ tìm hiểu xem vậy các tham số nay có tác dụng gì

Parameters

Name Required Description
follow optional Là danh sách các id người dùng sẽ theo dõi, sẽ được phân tách bằng dấu phẩy
track optional Là các từ khóa sẽ theo dõi, các từ khóa cũng sẽ được phân tách bởi dấu phấy
locations optional Ví trị của tin
stall_warnings optional Chỉ định xem có nên gửi cảnh báo ngừng hoạt động hay không
language optional Là ngôn ngữ sử dụng

https://sal.vn/v0jg4b

Và để xem chi tiết các thông số này chung ta có thể truy cập vào đường link này: https://sal.vn/DwDSNy

Sau khi tìm hiểu và sử dụng cho ứng dụng thì mình có một số chú ý cho các tham số mà chúng ta hay dùng như sau

  • Tham số Follow thì nó sẽ theo dõi các tweet:

    • Tweets created by the user - Các tweet do chính ta đang theo dõi đăng
    • Tweets which are retweeted by the user - Các tweet được người dùng này tweet lại
    • Replies to any Tweet created by the user - Bất kỳ Replies tới tweet do người dùng này tạo
    • Retweets of any Tweet created by the user - Bất kỳ Retweets lại Tweet do người dùng này tạo
    • Manual replies, created without pressing a reply button - Bất Kỳ trả lời thủ công nào mà không cần tag @
  • Track cần cẩn thận lọc sau khi truyền tham số này vào thì nó sẽ bắt tất cả các tweet, retweet, quote public mà thỏa mãn điều kiện nên nếu không lọc cẩn thận sau khi truyền tham số này server nhận rất nhiều spam

  • Điều quan trọng hơn nữa đó là khi ta đưa các tham số FollowTrack hoặc có thể nhiều tham số hơn thì chúng sẽ là phép "HOẶC - OR" chứ không phải là phép "VÀ - AND" đâu nha. Nên cũng cẩn thận lại nhận message liên hoàn cước 😄 😄 😄 ở phần sau đây mình sẽ hướng dẫn cách sử dụng công cụ trong go-twitter để lọc.

Trong Go-Twitter có một công cụ gọi là Demux nó được sử dụng để xử lý các message nhận về mình sẽ show ra cho mọi người nhìn nhá

    // Convenience Demux demultiplexed stream messages
	demux := twitter.NewSwitchDemux()
	demux.Tweet = func(tweet *twitter.Tweet) {
		fmt.Println("+-----------------------------------------------------------------------------------+")
		fmt.Println("| ")
		fmt.Println("|---Coordinates:  ", tweet.Coordinates)
		fmt.Println("|---CreatedAt:  ", tweet.CreatedAt)
		fmt.Println("|---CurrentUserRetweet:  ", tweet.CurrentUserRetweet)
		fmt.Println("|---DisplayTextRange:  ", tweet.DisplayTextRange)
		fmt.Println("|---Entities:  ", tweet.Entities)
		fmt.Println("|---ExtendedEntities:   ", tweet.ExtendedEntities)
		fmt.Println("|---ExtendedTweet:   ", tweet.ExtendedTweet)
		...
		fmt.Println("|---Source:   ", tweet.Source)
		fmt.Println("|---Text:   ", tweet.Text)
		fmt.Println("|---Truncated:   ", tweet.Truncated)
		fmt.Println("|---User:   ", tweet.User)
		fmt.Println("|---WithheldCopyright:   ", tweet.WithheldCopyright)
		fmt.Println("|---WithheldInCountries:   ", tweet.WithheldInCountries)
		fmt.Println("|---WithheldScope:    ", tweet.WithheldScope)
		fmt.Println("| ")
		fmt.Println("+-----------------------------------------------------------------------------------+")
		fmt.Println("\n")
	}
	demux.DM = func(dm *twitter.DirectMessage) {
		fmt.Println(dm.SenderID)
	}
	demux.Event = func(event *twitter.Event) {
		fmt.Printf("%#v\n", event)
	}
    
	.....
    

Khi đã có thể biết được những thông tin nào sẽ có thể nhận về, để biết rõ hơn về tên thuộc tính hay kiểu dữ liệu các bạn có thể dùng vscode tìm ra các struct của đối tượng trả về. Từ đó ta sẽ có thể thiết lập điều kiện lọc cho việc hiển thị hay bắn thông báo đến các nền tảng khác. Mình sẽ làm mẫu qua với bộ lọc của bot này nhá :

Chẳng hạn như ở đây tôi đang chỉ muốn lọc lấy các bài Tweet do người tôi đang theo dõi đăng hay các Retweet, Quote của người khác đăng lại bài viết của người mà đang được theo dõi thì điều kiện sẽ như sau. InReplyToStatusID = 0 là để loại bỏ các câu Reply, phần phân định RetweetedStatus khác nil có nghĩa đây là một bài Retweet lại, tweet.RetweetedStatus == niltweet.QuotedStatus != nil có nghĩa đây sẽ là một bài Quote, còn điệu kiện cuối cùng có nghĩa đây là một bài Tweet.

    ...
   
    // Convenience Demux demultiplexed stream messages
	demux := twitter.NewSwitchDemux()
	demux.Tweet = func(tweet *twitter.Tweet) {
		if tweet.InReplyToStatusID == 0 { // Không phải Reply
        
			if tweet.RetweetedStatus != nil {
                // Đây là một Retweet
			} else if tweet.RetweetedStatus == nil && tweet.QuotedStatus != nil {
                // Đây là một bài Quote
			} else if tweet.RetweetedStatus == nil && tweet.QuotedStatus == nil && {
                // Đây là một bài Tweet
			}
		}
	}

    ...

Vậy là ta đã thiết lập xong bộ lọc bây giờ sẽ biến tấu đi một chút bằng cách tạo thêm các route để thêm hoặc xóa các following và sau đó restart lại stream để nhận các kết quả mới. Nào chúng ta sẽ đi sang phần tạo route thay đổi stream nha

 

Tạo route và thiết lập restart stream

Phần nay mình sẽ không sử dụng database mà sử dụng file để lưu trữ cho nó gọn nhẹ. Nên đầu tiên ta sẽ chuyển hóa phần filter fix cứng thành load từ file.

    ...
    
    listFollowing, err := ioutil.ReadFile(".following")
	fmt.Println("listFollowing: ", strings.Split(string(listFollowing), ","))
	if err != nil {
		fmt.Println(err)
	}
    
    // FILTER
	filterParams := &twitter.StreamFilterParams{
		Follow:        strings.Split(string(listFollowing), ","),
		StallWarnings: twitter.Bool(true),
	}
	stream, err := client.Streams.Filter(filterParams)
	if err != nil {
		log.Fatal(err)
	}
    
    ...

Bây giờ để có thể restart lại stream mình sẽ thay vì để khởi tạo stream ở hàm main() thành biến thành một hàm riêng để có thể gọi lại việc khởi tạo stream.

https://sal.vn/IkGfMP

    ...

    var StreamTwitter *twitter.Stream
    var Demux twitter.SwitchDemux
    
    ...

    func CreateStreamTwitter() {
        flags := flag.NewFlagSet("user-auth", flag.ExitOnError)
        consumerKey := flags.String("consumer-key", os.Getenv("CONSUMER_KEY"), "Twitter Consumer Key")
        consumerSecret := flags.String("consumer-secret", os.Getenv("CONSUMER_SECRET"), "Twitter Consumer Secret")
        
        ...
        // Convenience Demux demultiplexed stream messages
        Demux = twitter.NewSwitchDemux()
        
        ...
        // FILTER
        filterParams := &twitter.StreamFilterParams{
            Follow:        strings.Split(string(listFollowing), ","),
            StallWarnings: twitter.Bool(true),
        }
        StreamTwitter, err = client.Streams.Filter(filterParams)
        if err != nil {
            log.Fatal(err)
        }
    }

    ...

Phần restart stream sẽ chỉ cần gọi lại gàm CreateStreamTwitter thì stream mới sẽ được khởi tạo và lưu vào biến toàn cục StreamTwitter

Tiếp theo sẽ viết thêm 2 route thêm và xóa following. Trước tiên là cần khởi tạo một server http để nhận về các request sau đó viết các route thực hiện add/remove. Ở đây mình sẽ sử dụng GET để dễ dàng cho việc test trên trình duyệt


    ...
    
    // Create stream twitter
	createStreamTwitter()
	go Demux.HandleChan(StreamTwitter.Messages)
    
	// Create server http
	fmt.Println("Starting Server")
	router := httprouter.New()
	router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
		fmt.Fprint(w, "Welcome!\n")
	})
	router.GET("/AddFollwing/:username", handleAddFollowing)
	router.GET("/RemoveFollwing/:username", handleRemoveFollowing)
	log.Fatal(http.ListenAndServe(":6868", router))
   
    ...
    
    //+-------------------------------------- Handle Request ----------------------------------------------+
    //|																									   |
    func handleAddFollowing(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
        username := p.ByName("username")
        userId := getUserIdFromUsernameTwitter(username)
        addToFile(userId, ".following")
    }
    func handleRemoveFollowing(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
        username := p.ByName("username")
        userId := getUserIdFromUsernameTwitter(username)
        removbeToFile(userId, ".following")
    }
    //|																									   |
    //+----------------------------------------------------------------------------------------------------+
    
    ...

    // Convenience Demux demultiplexed stream messages
	Demux = twitter.NewSwitchDemux()
	Demux.Tweet = func(tweet *twitter.Tweet) {
		if tweet.InReplyToStatusID == 0 {
			if tweet.RetweetedStatus != nil {
				println("\n")
				println("+---------------------------------- Retweet -------------------------------------------+")
				println("|																						|")
				println("|", tweet.RetweetedStatus.Text)
				println("|																						|")
				println("+--------------------------------------------------------------------------------------+")
				println("\n")
			} else if tweet.RetweetedStatus == nil && tweet.QuotedStatus != nil {
				println("\n")
				println("+----------------------------------- Quote --------------------------------------------+")
				println("|																						|")
				println("|", tweet.Text)
				println("|")
				println("|  +---------------------------------------------------------------------------------+ |")
				println("|  |                                                                                 | |")
				println("|  |", tweet.QuotedStatus.Text)
				println("|  |                                                                                 | |")
				println("|	+---------------------------------------------------------------------------------+ |")
				println("|																						|")
				println("+--------------------------------------------------------------------------------------+")
				println("\n")
			} else if tweet.RetweetedStatus == nil && tweet.QuotedStatus == nil {
				println("\n")
				println("+----------------------------------- Tweet --------------------------------------------+")
				println("|																						|")
				println("|", tweet.Text)
				println("|																						|")
				println("+--------------------------------------------------------------------------------------+")
				println("\n")
			}
		}

	}

Mình có thêm hàm sử dụng api để có thể từ username của người dùng Twitter mà tìm ra userId của người dùng và rồi đặt bộ lọc theo danh sách userId trong file .following.

    ...

    func getUserIdFromUsernameTwitter(_username string) string {
        // URL request
        reqURL := "https://api.twitter.com/2/users/by/username/" + _username

        token := os.Getenv("BEARER_TOKEN")

        client := &http.Client{}

        request, err := http.NewRequest("GET", reqURL, nil)
        if err != nil {
            log.Fatalln(err)
        }

        request.Header.Set("Authorization", "Bearer "+token)

        resp, err := client.Do(request)
        if err != nil {
            log.Fatalln(err)
        }

        body, _ := ioutil.ReadAll(resp.Body)
        var info InfoUser
        error := json.Unmarshal(body, &info)
        if error != nil {
            return ""
        }
        return info.Data.Id
    }

    ...

Bây giờ chúng ta sẽ test thử bằng cách start server và gọi route từ trình duyệt

  • Add following

  • Remove following

Tổng kết

Như vậy là chúng ta đã tạo ra được một con bot có khả năng subscribe Twitter những người ta muốn. Bài viết này mình mới chỉ dừng lại ở mức hiện thị ra màn hình console. Các bạn có thể cải tiến để thay vì hiển thị như thế nó có thể gửi message đến telegram, discord hay chatwork,... Ở bài viết sau mình cùng sẽ hướng dẫn cách mọi người kết hợp webhook của chatwork để bắt message cũng như nhân mệnh lệnh từ người dùng.

Code thì mình đã đẩy lên repo trên github mọi người có thể truy cập đường link sau để tham khảo, rất vui và hẹn gặp lại mọi người ở những bài viết tiếp theo : https://github.com/ngovannghia1997kma/bot-twitter-golang


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí