I found out the NFT police is real, so I made a bot and I'll tell you how to
Tags: python, twitter, googlecloud, programming
Published: 2022-01-22 23:15:28

12/02/2022 UPDATE I took down the bot it was fun while it lasted. I'll leave the article up for the memes and jokes.


I will explain how I built this Twitter bot [freenftbot](https://twitter.com/freenftbot), but first allow me to have to floor to tell you how I got here. Like all good internet stories it started with a meme.

{% youtube TFIc2V2_P3w %}

After seeing this meme I went down the rabbit hole and in a shocking turn of events. These scammy influencers got people on Twitter attacking people for using jpgs they don't own the NFT of.

So I thought it's time to take a stand against the **cringe mining, pixelated dildo jpg having, virtual hypebeasts**. It's time to use my unique set of skills and terrible prioritization of time and build a Twitter bot.

### So you might be asking what does the bot do?
It's simple you follow the instructions here: (I have screenshots of people failing this)
![Image "Bot tweet"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zdciqgnskzi8170s1za7.png) And Profit xD:
![Image "NFT bro reply"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/van9kp655ngcbq9y15ab.png)

---


## How I did it and you can too
To be clear I'm going going to give tips around places I got stuck and skip the straight forward parts or easy to google parts.

### Perquisites
- Some python skills
- Twitter developer access for the API get it and apply for ELEVATED access so you get access to the V1 API because V2 doesn't have media upload yet. [here](https://developer.twitter.com/) (I create a separate account for my bot. I would say do that too, but live your life Queen)
- Set up Google cloud project if you haven't used your free tier look into it. I'll explain parts of it, but google is your friend.
- Some NFT images I grabed this [kaggle dataset](https://www.kaggle.com/stanleyjzheng/bored-apes-yacht-club)

To kick off these are my global scope variables
```python
import tweepy
import config # I put secrets and tokens here
# client is the V2 API
client = tweepy.Client(
    bearer_token=config.BEARER_TOKEN,
    consumer_key=config.KEY,
    consumer_secret=config.SECRET,
    access_token=config.ACCESS_TOKEN,
    access_token_secret=config.ACCESS_TOKEN_SECRET,
)

# api is the V1 API
auth = tweepy.OAuthHandler(config.KEY, config.SECRET)
auth.set_access_token(config.ACCESS_TOKEN, config.ACCESS_TOKEN_SECRET)
api = tweepy.API(auth)

os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "<google service account key json>"
storage_client = storage.Client("<google cloud project>")
bucket = storage_client.bucket("<google storage bucket>")  # where all the NFT images are
```  The documentation is actually really good [here](https://docs.tweepy.org/en/stable/index.html)

One neat thing about google cloud functions, if the environment hasn't been deleted it'll reuse this variables so they won't be re-assigned every run. Smaller carbon footprint. #OurPlanet

Now how do you get all mentions from the last 5 minutes?
`client.get_users_mentions(id=ID, start_time=<time 5 mins ago>)`
now do it again, but handle pagination
```python
def get_nft_mentions(start_time, max=100):
    nft_tweets = set() # avoid duplicates
    response = client.get_users_mentions(id=ID, max_results=max, start_time=start_time)
    next_token = True
    while next_token:
        tweets = response.data
        if tweets != None:
            for tweet in tweets:
                if does_text_have_hashtag(tweet.text) and is_following(tweet.author_id):
                    nft_tweets.add(tweet)
        if "next_token" in response.meta:
            response = response = client.get_users_mentions(
                id=ID,
                max_results=max,
                tweet_fields=["author_id"],
                start_time=start_time,
                pagination_token=response.meta["next_token"],
            )
        else:
            next_token = False
    return nft_tweets
```
We have a set of tweets to loop over. We just need to grab a random NFT from our bucket that's easy using our neatfy csv file of all of our NFTs we got with the images and some pandas:
```python
def download_file(file_name):
    destination_file = "/tmp/" + file_name # You can use tmp in cloud function for tmp files just like the NFT images for this
    blob = bucket.blob(file_name)
    blob.download_to_filename(destination_file)
    return destination_file

def pick_nft(nft_frame):
    nft_row = nft_frame.sample(n=1)
    nft_hash = nft_row["IPFS HASH"].iloc[0]
    nft_file = download_file(f"{nft_hash}.jpg")
    return nft_file, nft_row["INITIAL SEQUENCE INDEX"].iloc[0]


def get_media_id(nft):
    return api.simple_upload(nft).media_id_string # Remember api. uses the V1 API client. uses the V2 API. V2 doesn't have media upload (yet)
```
So we have a tweet to reply to and a image uploaded ready to reply with. 
![Image ppap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cocy7gyef85y0onx3no0.gif)(dead meme lol)

```python
def reply_with_nft(tweet, media_id):
    return client.create_tweet(in_reply_to_tweet_id=tweet.id, media_ids=[media_id])
```

put it all together in a main function

```python
def main(request): # Need a parameter for cloud functions
    csv_file = download_file("hashes.csv")
    nft_frame = pd.read_csv(csv_file)
    tweets = get_nft_mentions(get_time_five_mins_ago())

    errors = []
    for tweet in tweets:
        # chosen NFT and upload it
        nft_path, nft_index = pick_nft(nft_frame)
        media_id = get_media_id(nft_path)

        # reply to tweet
        response = reply_with_nft(tweet, media_id)

        # if response is clean all user to cd file and remove nft
        if len(response.errors) == 0:
            nft_frame.drop(nft_index, axis=0, inplace=True)
        else:
            print(response.errors)
            errors.append(response.errors)

        nft_frame.to_csv(csv_file, index=False)
        upload_file(csv_file)

    if len(errors) >= 0: 
        return "There's some problems boss" # you need a return statement for cloud functions
    else:
        return "all good"
```
AND THERE YOU HAVE IT! it's not the prettiest cloud function, but it's a valid cloud function. We set the bar at "it works kinda" around here. Now the one I have in production had a few extra bells and whistles, but let's not get caught up with the devil in the details.

### The Power of the cloud

First we need a cloud function. You can work it out! I believe in you. Well I don't know you, so I don't, but try anyways. There's a few things I can give you tips on.

#### Cloud Function Tips:
- uncheck "Require HTTPS" is just causes issues I couldn't get it work, but good luck if you want it.
- If you choose to upload your project through zip the main file needs to be in the root of the zip file.
- Under "Runtime, build, connections and security settings" the time out is defaulted to 60 seconds there's a max of 540 seconds.
- Error messages are found under the details tab.

If all goes well and you have a green tick. We now have:
{% youtube e_DqV1xdf-Y %}

But how do we use it? For this case I used Cloud Scheduler!

Pick a time frequency. Under "Configure the execution" Target Type is HTTP URL you get that from the cloud function then add a "OIDC TOKEN" under Auth Header for the Audience use the HTTP URL again (I don't get it either) and hit the blue button and hope for the best.

#### Tips if it fails:
- If the error is "internal" check your cloud function logs.
- If the error is "Permission Denied" your service account permissions are fried. - Confucius (art of war)


Thanks you for reading I hope I made you laugh because God help you if you found me helpful. If you have nothing else better to do go follow me on twitter [@itsChoudhry](https://twitter.com/ItsChoudhry)


Lastly I would like to give credit to a Twitter friend who said "Building a twitter bots is fun" - [Talal](https://twitter.com/Talal916) check out his bots [FriendCircleBot](https://twitter.com/friendcirclebot) and [WordCloudBot](https://twitter.com/wordcloudsbot)