How to build a Twitter API v2 bot with AWS Lambda, and GoLang
Toul
Posted on October 19, 2022
Today I am introducing the InfraHamBurglar Twitter Bot, which is reporting on the latest "Robble, Robble" worldwide.
As someone who works in cybersecurity, I thought it would be funny to have a Twitter bot named the "InfraHamBurglar" as a pun on an Infrastructure Burglar that is going Ham. Where Infrastructure loosely means Servers and Databases that exist in the cloud.
For those unfamiliar, the HamBurglar is a McDonald's character from the past that is known for stealing hamburgers. The bot's job is to Tweet out the latest CyberAttacks and DataBreaches with the phrase "Robble, Robble, Good Gobble." and a link to tweet.
Let's get to it.
1. Sign - Up for a Developer Account for Twitter Developer Portal
Go here and provide the necessary information to create a developer account.
Next, create a project and name it whatever you want. In my case, it is InfraHamBurglar. Then, once the project is completed, go ahead and generate the API KEY and API SECRET
Then apply for an elevated permissions account so your bot can *tweet otherwise, your bot will only be able to READ tweets.
The time varies on approval, and you'll probably have to go back and forth via e-mail with Twitter Support to get approved. It took me about 3 days for me.
2. Code with gotwi api v2 pkg
Thankfully the wonderful GoTwi package exists and has efficiently wrapped all the URL endpoints for Twitter API v2.
The Package author even provided working examples that we'll use to cobble together the bot.
2.1 Create main.go file
package main
import (
"fmt"
"net/http"
"os"
"time"
"github.com/michimani/gotwi"
)
func main() {
args := os.Args
if len(args) < 2 {
fmt.Println("The 1st parameter for command is required. (create|stream)")
os.Exit(1)
}
command := args[1]
switch command {
case "list":
// list search stream rules
listSearchStreamRules()
case "delete":
// delete a specified rule
if len(args) < 3 {
fmt.Println("The 2nd parameter for rule ID to delete is required.")
os.Exit(1)
}
ruleID := args[2]
deleteSearchStreamRules(ruleID)
case "create":
// create a search stream rule
if len(args) < 3 {
fmt.Println("The 2nd parameter for keyword of search stream rule is required.")
os.Exit(1)
}
keyword := args[2]
createSearchStreamRules(keyword)
case "stream":
// exec filtered stream API
execSearchStream()
default:
fmt.Println("Undefined command. Command should be 'create' or 'stream'.")
os.Exit(1)
}
}
// newGotwiClientWithTimeout creates a new gotwi.Client
// that has custom http.Client with arbitrary timeout.
func newGotwiClientWithTimeout(timeout int) (*gotwi.Client, error) {
in := &gotwi.NewClientInput{
AuthenticationMethod: gotwi.AuthenMethodOAuth2BearerToken,
HTTPClient: &http.Client{
Timeout: time.Duration(timeout) * time.Second,
},
}
return gotwi.NewClient(in)
}
and also create
2.1 filterered_stream.go
package main
import (
"context"
"fmt"
"github.com/michimani/gotwi"
"github.com/michimani/gotwi/tweet/filteredstream"
"github.com/michimani/gotwi/tweet/filteredstream/types"
)
// createSearchStreamRules lists search stream rules.
func listSearchStreamRules() {
c, err := newGotwiClientWithTimeout(30)
if err != nil {
fmt.Println(err)
return
}
p := &types.ListRulesInput{}
res, err := filteredstream.ListRules(context.Background(), c, p)
if err != nil {
fmt.Println(err.Error())
return
}
for _, r := range res.Data {
fmt.Printf("ID: %s, Value: %s, Tag: %s\n", gotwi.StringValue(r.ID), gotwi.StringValue(r.Value), gotwi.StringValue(r.Tag))
}
}
func deleteSearchStreamRules(ruleID string) {
c, err := newGotwiClientWithTimeout(30)
if err != nil {
fmt.Println(err)
return
}
p := &types.DeleteRulesInput{
Delete: &types.DeletingRules{
IDs: []string{
ruleID,
},
},
}
res, err := filteredstream.DeleteRules(context.TODO(), c, p)
if err != nil {
fmt.Println(err.Error())
return
}
for _, r := range res.Data {
fmt.Printf("ID: %s, Value: %s, Tag: %s\n", gotwi.StringValue(r.ID), gotwi.StringValue(r.Value), gotwi.StringValue(r.Tag))
}
}
// createSearchStreamRules creates a search stream rule.
func createSearchStreamRules(keyword string) {
c, err := newGotwiClientWithTimeout(30)
if err != nil {
fmt.Println(err)
return
}
p := &types.CreateRulesInput{
Add: []types.AddingRule{
{Value: gotwi.String(keyword), Tag: gotwi.String(keyword)},
},
}
res, err := filteredstream.CreateRules(context.TODO(), c, p)
if err != nil {
fmt.Println(err.Error())
return
}
for _, r := range res.Data {
fmt.Printf("ID: %s, Value: %s, Tag: %s\n", gotwi.StringValue(r.ID), gotwi.StringValue(r.Value), gotwi.StringValue(r.Tag))
}
}
// execSearchStream call GET /2/tweets/search/stream API
// and outputs up to 10 results.
func execSearchStream() {
c, err := newGotwiClientWithTimeout(120)
if err != nil {
fmt.Println(err)
return
}
p := &types.SearchStreamInput{}
s, err := filteredstream.SearchStream(context.Background(), c, p)
if err != nil {
fmt.Println(err)
return
}
cnt := 0
for s.Receive() {
t, err := s.Read()
if err != nil {
fmt.Println(err)
} else {
if t != nil {
cnt++
fmt.Println(gotwi.StringValue(t.Data.ID), gotwi.StringValue(t.Data.Text))
}
}
if cnt > 10 {
s.Stop()
break
}
}
}
2.2 Create .env file
For the code to work it'll need to have local variables available if you're running from your personal laptop for testing purposes.
GOTWI_API_KEY=<YOUR_API_KEY>
GOTWI_API_KEY_SECRET=<YOUR_API_KEY_SECRET>
GOTWI_ACCESS_TOKEN=<YOUR_ACCESS_TOKEN>
GOTWI_ACCESS_TOKEN_SECRET=<YOUR_ACCESS_TOKEN_SECRET>
N.B. Export the vars with this line to your local terminal if you want to test locally as you develop.
> for line in $(cat .env); do export $line; done
2.3 Run code to create filtered stream term
go run main.go create <name-of-your-term>
This will create a term that the Twitter API v2 filtered stream will filter out when searching through tweets. You may also add other terms as well and the stream will also capture those tweets as well.
Now, that the filter term(s) have been created let's code up the bot.
2.4 Altering the code for the InfraHamburglarBot
I am choosing to use AWS Lambda because it is the cheapest option that I'm familiar with. A quick calculation based on the AWS Lambda pricing docs shows that this project will only cost me under $2 USD each month.
cost of lambda every 15 mins => 120000 ms X 0.0000000021 cents/ms x 96 executions per day
Which is much cheaper than running a small cloud server in terms of cost, maintenance, and security. A lambda is only up for barely 2 minutes versus a server that runs 1440 minutes a day (lots of time for a curious InfraHamBurglar to try and exploit).
All that the reader should need to change is the USER
and what text they want their bot to tweet when retweeting a tweet; the line that reads
p := "Robble, Robble, Good Gobble " + "https://twitter.com/" + gotwi.StringValue(t.Data.AuthorID) + "/status/" + gotwi.StringValue(t.Data.ID)
Also, delete the filtered_streams.go
file as it isn't needed and will add to the Zip file size if included.
However, if the reader wishes to change what the bot does retweet, direct message, reply, and so on then, this code should serve as a decent starting point.
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
"github.com/michimani/gotwi"
"github.com/michimani/gotwi/fields"
"github.com/michimani/gotwi/tweet/filteredstream"
"github.com/michimani/gotwi/tweet/filteredstream/types"
"github.com/michimani/gotwi/tweet/managetweet"
twt "github.com/michimani/gotwi/tweet/managetweet/types"
"net/http"
"os"
"strings"
"time"
)
const (
OAuthTokenEnvKeyName = "GOTWI_ACCESS_TOKEN"
OAuthTokenSecretEnvKeyName = "GOTWI_ACCESS_TOKEN_SECRET"
USER = "1510513502628331522"
)
// SimpleTweet posts a tweet with only text, and return posted tweet ID.
func SimpleTweet(c *gotwi.Client, text string) (string, error) {
p := &twt.CreateInput{
Text: gotwi.String(text),
}
res, err := managetweet.Create(context.Background(), c, p)
if err != nil {
return "", err
}
return gotwi.StringValue(res.Data.ID), nil
}
func newOAuth1Client() (*gotwi.Client, error) {
in := &gotwi.NewClientInput{
AuthenticationMethod: gotwi.AuthenMethodOAuth1UserContext,
OAuthToken: os.Getenv(OAuthTokenEnvKeyName),
OAuthTokenSecret: os.Getenv(OAuthTokenSecretEnvKeyName),
}
return gotwi.NewClient(in)
}
// execSearchStream call GET /2/tweets/search/stream API
// and outputs up to 10 results.
func execSearchStream() {
c, err := newGotwiClientWithTimeout(120)
if err != nil {
fmt.Println(err)
return
}
p := &types.SearchStreamInput{
TweetFields: fields.TweetFieldList{
fields.TweetFieldAuthorID,
},
}
s, err := filteredstream.SearchStream(context.Background(), c, p)
if err != nil {
fmt.Println(err)
return
}
cnt := 0
for s.Receive() {
t, err := s.Read()
if err != nil {
fmt.Printf("ERR: ", err)
} else {
if t != nil {
if gotwi.StringValue(t.Data.AuthorID) != USER {
if !strings.Contains(gotwi.StringValue(t.Data.Text), "RT") {
cnt++
oauth1Client, err := newOAuth1Client()
if err != nil {
panic(err)
}
p := "Robble, Robble, Good Gobble " + "https://twitter.com/" + gotwi.StringValue(t.Data.AuthorID) + "/status/" + gotwi.StringValue(t.Data.ID)
tweetID, err := SimpleTweet(oauth1Client, p)
if err != nil {
panic(err)
}
fmt.Println("Posted tweet ID is ", tweetID)
}
}
}
if cnt > 10 {
s.Stop()
break
}
}
}
}
func newGotwiClientWithTimeout(timeout int) (*gotwi.Client, error) {
in := &gotwi.NewClientInput{
AuthenticationMethod: gotwi.AuthenMethodOAuth2BearerToken,
HTTPClient: &http.Client{
Timeout: time.Duration(timeout) * time.Second,
},
}
return gotwi.NewClient(in)
}
func main() {
lambda.Start(execSearchStream)
}
3. Deploying to AWS Lambda
Now, that the code is ready and unnecessary files have been removed let's get to the infrastructure side of this project.
3.1 Zipping
As I previously shared under section II.c Build and push the package to S3 bucket AWS requires the lambda to be of a certain form when created, and that means build as well so use this shell script to build the *.zip for your bot.
> GOOS=linux GOARCH=amd64 go build -o your-awesome-bot main.go
> zip your-awesome-bot.zip your-awesome-bot
Now, that the zip has been made head over to your AWS console and go to Lambda in the AWS Console.
3.2 Creating lambda
Select Create new lambda
Name it
Once it has been created go to Code Tab >Runtime Settings > Handler > and change the name from hello
to <your-awesome-bot>
3.3 Set Up Env Variables
Next, go to Configuration Tab > Environment Variables > Add Variables.
Add all the variables from your .env file.
In the end, there should be four present.
3.4 General Configuration
Now go to Configuration Tab > General Configuration > Edit to change the default values.
Change memory to the min of 128 MB
Change the default time out to 3 minutes
Leave default Ephemeral storage size (can't change it)
This will save you money in the long run.
3.3 Setting up EventBridge Rule
Now, Click Triggers on the AWS Lambda and select EventBridge.
Select Create New rule and add in the info you feel is important.
Lastly, input your cron() expression which will trigger the lambda every so often.
I personally opted for running the bot every 15 mins.
Conclusion
Now, you have a general framework for constructing Twitter bots in GoLang. Please consider giving the InfraHamburglarBot and me a follow if you found the article helpful.
Posted on October 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.