Created
January 18, 2020 21:13
-
-
Save NuroDev/bfaa560bd53ce5164de3818f853966f6 to your computer and use it in GitHub Desktop.
πΌ Cosmo - A simple twitter bot to upload/download an image repeatedly. Used to analyze Twitter image compression results
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "cosmo" | |
version = "1.1.1" | |
authors = ["N U R O β’"] | |
edition = "2018" | |
[dependencies] | |
egg-mode = "0.13.0" | |
failure = "0.1.6" | |
reqwest = "0.9.22" | |
tokio = "0.1.22" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use egg_mode::{ | |
media::{media_types, MediaHandle, UploadBuilder}, | |
tweet::{delete, DraftTweet, Tweet}, | |
KeyPair, Response, Token, | |
}; | |
use failure::{err_msg, Error}; | |
use std::{ | |
ffi::OsStr, | |
fs::{copy as FsCopy, create_dir_all, File}, | |
io::{copy as IoCopy, Read}, | |
path::{Path, PathBuf}, | |
thread::sleep, | |
time::{Duration, Instant}, | |
}; | |
use structopt::StructOpt; | |
use tokio::runtime::current_thread::block_on_all; | |
const CONSUMER_KEY: &str = ""; | |
const CONSUMER_SECRET: &str = ""; | |
const ACCESS_KEY: &str = ""; | |
const ACCESS_SECRET: &str = ""; | |
const OUTPUT_DIR: &str = "./dist"; | |
#[derive(Debug, StructOpt)] | |
#[structopt( | |
name = "cosmo", | |
about = "A simple twitter bot to upload/download an image repeatedly. Used to analyze Twitter image compression results" | |
)] | |
struct CLI { | |
/// The path to the entry image | |
#[structopt(name = "image_path", parse(from_os_str))] | |
image_path: PathBuf, | |
/// Number of recursions (Times to upload/download the image) | |
#[structopt(name = "recursions")] | |
recursions: u64, | |
} | |
fn upload_media(path: &Path, token: &Token) -> Result<MediaHandle, Error> { | |
// Get the image file extension | |
let file_extension = match path | |
.extension() | |
.and_then(OsStr::to_str) | |
.expect("β Error parsing file extension to string") | |
{ | |
"jpg" => media_types::image_jpg(), | |
"png" => media_types::image_png(), | |
"gif" => media_types::image_gif(), | |
"webp" => media_types::image_webp(), | |
_ => panic!("β Invalid file type submitted"), | |
}; | |
// Load image into buffer | |
let mut buffer: Vec<u8> = Vec::new(); | |
let mut _file: usize = File::open(path)?.read_to_end(&mut buffer)?; | |
// Create new UploadBuilder instance | |
let builder: UploadBuilder = UploadBuilder::new(buffer, file_extension); | |
// Upload the image | |
match block_on_all(builder.call(&token)) { | |
Ok(media) => { | |
println!("β¬οΈ Uploaded media"); | |
Ok(media) | |
} | |
Err(err) => panic!("β Error uploading media: {:#?}", err), | |
} | |
} | |
fn send_tweet( | |
media_id: u64, | |
token: &Token, | |
file_name: &str, | |
i: u64, | |
) -> Result<Response<Tweet>, Error> { | |
let draft: DraftTweet = | |
DraftTweet::new(format!("πΈ Image: {}\nβΏ Loop: {}", file_name, i)).media_ids(&[media_id]); | |
return match block_on_all(draft.send(&token)) { | |
Ok(tweet) => { | |
println!("βοΈ Tweet sent"); | |
Ok(tweet) | |
} | |
Err(e) => { | |
println!("β Error tweeting iter {}: {:#?}", i, e); | |
// Check if it is because we hit the daily API limit (Code 185) | |
// If so, wait 24 hours and retry, if not, return an error | |
match &e { | |
egg_mode::error::Error::TwitterError(err) => { | |
if err.errors[0].code == 185 { | |
println!("β οΈ API Limit reached\n ποΈ Sleeping for 24 hours"); | |
sleep(Duration::new(60 * 60 * 24, 0)); | |
Ok(send_tweet(media_id, token, file_name, i)?) | |
} else { | |
Err(err_msg(e)) | |
} | |
} | |
_ => Err(err_msg(e)), | |
} | |
} | |
}; | |
} | |
fn download_media( | |
tweet: &Response<Tweet>, | |
file_name: &str, | |
file_extension: &str, | |
i: u64, | |
) -> Result<(), Error> { | |
let mut response = reqwest::get( | |
&tweet | |
.response | |
.entities | |
.media | |
.clone() | |
.expect("β No media url found")[0] | |
.media_url_https, | |
)?; | |
let mut output_image = File::create(&format!( | |
"{}/{}_{}.{}", | |
OUTPUT_DIR, | |
file_name, | |
i + 1, | |
file_extension | |
))?; | |
match IoCopy(&mut response, &mut output_image) { | |
Ok(_) => Ok(println!("β¬οΈ Downloaded media")), | |
Err(e) => Err(err_msg(format!("β Error downloading image: {:#?}", e))), | |
} | |
} | |
fn main() -> Result<(), Error> { | |
// Get CLI arguments | |
let cli = CLI::from_args(); | |
println!("============================================"); | |
// Generate twitter access token using credentials provided above | |
let token: Token = Token::Access { | |
consumer: KeyPair::new(CONSUMER_KEY, CONSUMER_SECRET), | |
access: KeyPair::new(ACCESS_KEY, ACCESS_SECRET), | |
}; | |
let file_name: &str = &cli | |
.image_path | |
.file_stem() | |
.and_then(OsStr::to_str) | |
.expect("β Error unwrapping image name"); | |
let file_extension: &str = &cli | |
.image_path | |
.extension() | |
.and_then(OsStr::to_str) | |
.expect("β Error unwrapping image extension"); | |
// Create the output directory | |
create_dir_all(OUTPUT_DIR).expect("βοΈ Error creating output directory"); | |
println!("β¨ Created output directory"); | |
// Copy the entry image to the output directory | |
let entry_image_path = format!("{}/{}_0.{}", OUTPUT_DIR, file_name, file_extension); | |
match FsCopy(&cli.image_path, entry_image_path) { | |
Ok(_) => println!("β‘οΈ Copied entry image"), | |
Err(err) => panic!("β Error copying entry image: {:#?}", err), | |
}; | |
// Start the stopwatch | |
let timer = Instant::now(); | |
println!("============================================"); | |
for i in 0..cli.recursions { | |
println!("βΏ Loop: {}", i); | |
// Upload the image and get back the media id | |
let media: MediaHandle = upload_media( | |
Path::new(&format!( | |
"{}/{}_{}.{}", | |
OUTPUT_DIR, file_name, i, file_extension | |
)), | |
&token, | |
)?; | |
// Send the tweet | |
let tweet: Response<Tweet> = send_tweet(media.id, &token, &file_name, i)?; | |
// Download the uploaded image | |
download_media(&tweet, &file_name, &file_extension, i)?; | |
// Delete tweet after the media has been downloaded | |
match block_on_all(delete(tweet.id, &token)) { | |
Ok(_) => println!("ποΈ Deleted tweet"), | |
Err(err) => panic!("β Error deleting tweet {}: {:#?}", i, err), | |
}; | |
println!("β Done!"); | |
println!("============================================"); | |
} | |
println!("β° Time: {} seconds", timer.elapsed().as_secs()); | |
println!("============================================"); | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
πΌ Cosmo
A simple twitter bot to upload/download an image repeatedly. Used to analyze Twitter image compression results
π Usage
Create a new Rust project named whatever you like. Then copy the code from both the
Cargo.toml
andmain.rs
file above and paste into your newly generated projectsCargo.toml
andmain.rs
.You will then need to enter your Twitter developer API credentials. To do this, open the
main.rs
file and at the top, add your credentials to the following chunk:Once you have added your credentials, run the following command with your chosen parameters to run Comso:
For example:
After the application has finished, all screenshots will be available in your project directory in a directory named
dist
π§ Parameters:
IMAGE PATH
: Path to the entry image that will be uploaded firstRECURSIONS
: Number of recursions / times to repeatedly upload/download the image