Head-first in Rust - Tweet Proof of Concept
5 min read

Head-first in Rust - Tweet Proof of Concept

Head-first in Rust - Tweet Proof of Concept

Last year I thought of learning a new programming language. I was between Rust and Go and started to read reviews and use cases for both, to figure out which one should I pick. After spending few weeks on reading comparisons, I still couldn't figure which one is better, So I remembered something I used to tell colleagues praising one language or a framework over another one:

They are only tools for solving a problem. The trick is to find the one that allows you to resolve the problem quicker.

So... I wanted for a while to write a program that connects to my blog, and tweets when I start writing an article or when I publish one. I tried to do it first in NodeRed. I could get the flow triggered, have the data in JSON, but it seemed to me a bit over-the-hand to convert the JSON to what the Twitter API wanted, not to mention the pain of adding an image.

So, I looked at picking up the language from a "which is better fitted to let me write a micro-service that will allow me to tweet when stuff changes in my blog? From all assessments I've read, Go seemed the easier choice, but then I read some performance stuff, and I picked up Rust :) Oh, well...

Prerequisites

I've got RustRover, which is beta at the moment of writing and free. I've downloaded rustup and installed rust locally. I must admit I wanted to do all development in WLS, but I found the experience sub-optimal, so I ended up with Windows exclusive setup.

I also made sure I have cargo installed.

Initial setup

I have created a new project via RustRover, but you can as easily create one via cargo:

cargo new twitter-example

Its structure should be something like this:

This is the most basic structure you can find, and it fit my purpose of sending a tweet :)

The cargo file is:

[package]  
name = "twitter-example"  
version = "0.1.0"  
edition = "2021"  
  
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html  
  
[dependencies]  
time = { version = "0.3.31", features = ["macros"] }  
twitter-v2 = "0.1.8"  
tokio = { version = "1.35.1", features = ["rt", "rt-multi-thread", "macros"] }  
anyhow = "1.0.79"  
futures = "0.3.30"  
tracing = "0.1.40"  
axum = "0.7.4"  
twitter-api-v1 = "0.2.1"  
oauth1-twitter = "0.2.1"  
dotenvy = "0.15.7"  
  
reqwest = { version = "0.11" }  
  
pretty_env_logger = { version = "0.5" }

As you can see, I've already added here the tokio crate, which gives me the capability to call async functions and the twitter-v2 to connect to twitter.

I've added a dependency of dotenvy to allow me to add configuration variables in a .env file. I've also created the .env file:

CONSUMER_KEY=<twitter_consumer_key> 
CONSUMER_SECRET=<twitter_consumer_secret>   
ACCESS_TOKEN=<twitter_access_token>  
ACCESS_TOKEN_SECRET=<twitter_access_token_secret> 
  
RUST_LOG=trace  
  
STATUS="Tweet from my first Rust bot thing!"

This will give me the twitter API keys and the STATUS variable will give me the message to be sent. All these keys you can get from the Twitter developer portal, once you create an app. Note that, following the best practices in security, you can only see the keys once when they're generated. So make sure you copy them somewhere!

The code

The code is simpler than I thought (note here that I only send a text tweet, no image or other metadata!). The list of "imports" (python terminology here) is:

use std::env;  
use twitter_api_v1::endpoints::tweets::manage_tweets::create_tweet;  
use twitter_api_v1::endpoints::EndpointRet;  
use twitter_api_v1::TokenSecrets;

The main function has the following signature:

#[tokio::main]  
async fn main() -> Result<(), Box<dyn std::error::Error>> {

This had to be made async because the twitter API is asynchronous. The tokio::main decorator tells Rust to practically convert the async into a synchronous function and wait for the response.

Getting the keys

I'm using the .env approach as defined above:

dotenvy::dotenv().ok();  
  
let status = env::var("STATUS").expect("STATUS not found in .env file");  
  
let consumer_key = env::var("CONSUMER_KEY").expect("CONSUMER_KEY not found in .env file");  
let consumer_secret =  
    env::var("CONSUMER_SECRET").expect("CONSUMER_SECRET not found in .env file");  
let access_token = env::var("ACCESS_TOKEN").expect("ACCESS_TOKEN not found in .env file");  
let access_token_secret =  
    env::var("ACCESS_TOKEN_SECRET").expect("ACCESS_TOKEN_SECRET not found in .env file");

This will look for environment variables first as declared in the execution environment and then cascade to the .env file. Cool! We already have them defined!

Once this is done, I'm already moving to the Twitter API and create a TokenSecrets object:

let token_secrets = TokenSecrets::new(  
    consumer_key,  
    consumer_secret,  
    access_token,  
    access_token_secret,  
);

Then, I create a client via reqwest::Client::builder() function:

let client = reqwest::Client::builder()  
    .connection_verbose(env::var("RUST_LOG").map(|x| x.starts_with("trace")) == Ok(true))  
    .danger_accept_invalid_certs(true)  
    .build()?;

Now, we're ready to create_tweet:

let ret = create_tweet(&token_secrets, client, Some(&status), None, None).await?;

Note that we're await-ing for the reply of this function. Once we have the reply, we can process it, to see if everything is OK:

match ret {  
    EndpointRet::Ok(ok_json) => {  
        println!("create_tweet:{ok_json:?}");  
    }  
    _x => panic!("{_x:?}"),  
};

The last thing is:

Ok(())

Cool! Now once you run the program, you'll be able to send a tweet!

Full code

The full main.rs code is:

use std::env;  
use twitter_api_v1::endpoints::tweets::manage_tweets::create_tweet;  
use twitter_api_v1::endpoints::EndpointRet;  
use twitter_api_v1::TokenSecrets;  
  
#[tokio::main]  
async fn main() -> Result<(), Box<dyn std::error::Error>> {  
    dotenvy::dotenv().ok();  
  
    let status = env::var("STATUS")
        .expect("STATUS not found in .env file");  
  
    let consumer_key = env::var("CONSUMER_KEY")
        .expect("CONSUMER_KEY not found in .env file");  
    let consumer_secret =  
        env::var("CONSUMER_SECRET")
        .expect("CONSUMER_SECRET not found in .env file");  
    let access_token = env::var("ACCESS_TOKEN")
        .expect("ACCESS_TOKEN not found in .env file");  
    let access_token_secret =  
        env::var("ACCESS_TOKEN_SECRET")
        .expect("ACCESS_TOKEN_SECRET not found in .env file");  
  
    let token_secrets = TokenSecrets::new(  
        consumer_key,  
        consumer_secret,  
        access_token,  
        access_token_secret,  
    );  
  
  
    let client = reqwest::Client::builder()  
        .connection_verbose(env::var("RUST_LOG")
        .map(|x| x.starts_with("trace")) == Ok(true))  
        .danger_accept_invalid_certs(true)  
        .build()?;  
  
    let ret = create_tweet(
        &token_secrets, client, Some(&status), None, None
    ).await?;  
  
    match ret {  
        EndpointRet::Ok(ok_json) => {  
            println!("create_tweet:{ok_json:?}");  
        }  
        _x => panic!("{_x:?}"),  
    };  
  
    Ok(())  
}

Easy peasy! Now the next step is to transform this hello tweet world into a service.

HTH,