惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
博客园_首页
博客园 - 【当耐特】
V
Visual Studio Blog
博客园 - 叶小钗
月光博客
月光博客
美团技术团队
J
Java Code Geeks
小众软件
小众软件
Y
Y Combinator Blog
博客园 - Franky
Martin Fowler
Martin Fowler
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
Microsoft Security Blog
Microsoft Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG

Cocoa

Untitled | Cocoa PicoBf: A brainf**k lang REPL environment for Pico Pi Compile LLVM with OpenMP on macOS and Linux My Virtualization Setup | Cocoa Doodles | Cocoa Hardware UART communication between Raspberry Pi 4 and Arduino Micro The first amazing thing in 2021 OK Google, set Telegram as my YouTube music player! Record YouTube Live Stream | Cocoa 可能隨時咕咕咕掉的 NLP 項目(1)—— 抓取 YouTube Live Chat NextDest | Cocoa macOS version of Twitter Image Saver Notes on Differential Equations (4) —— Step Function $H(t)$ and Delta Function $\delta(t)$ Notes on Differential Equations (3) —— $\frac{dy}{dt}=ay+q(t)$
Rust Learning from Zero (26) —— Save Twitter Images
Cocoa · 2020-10-28 · via Cocoa

I followed a few Twitter accounts that post yuri photos, and I saved some of these images when I saw them. 女の子になりたい!

However, it would be better if there is a bot that could do all this for me. And of course, applying for a Twitter developer account is a must. When your developer account is ready, you will need to generate a consumer key & secret pair.

This program will be really simple and straightforward. It reads from a config file that contains the consumer key & secret pair, as well as a list of usernames you interested in, and the path to the directory where you'd like to save images. A sample of the config file is shown below.

{
  "key": "KEYKEYKEYKEYKEYKEYKEYKEY",
  "secret": "SECRETSECRETSECRETSECRET",
  "img_dir": "imgs",
  "users": [
    "username1",
    "username2",
    "username3"
  ]
}

Substitute your own consumer key & secret, and then list some users you interested in in the users array.

Also, I don't want the program to be too complex, so I decide to not use any database. If you want to add more users to the list while the program is running, then just add them into the JSON file and save it. The program will try to reload the user list before the next round of polling data.

And this program is on OwO now! https://owo.ryza.moe/. You can download and install it on Linux with only one line command. (For more details about OwO, you can refer to this post, Rust Learning from Zero (23) —— The OwO software distribution system!)

bash <(curl -sSLf https://owo.ryza.moe/twitter-img-saver/install.sh)

Below comes the code!

use clap::Clap;
use egg_mode::{entities::MediaType, Token, tweet::{Timeline, Tweet}};
use hyper::{body::HttpBody, Client};
use hyper_tls::HttpsConnector;
use serde::Deserialize;
use std::{
    collections::HashMap,
    fs::File,
    io::BufReader,
    path::Path,
    str::FromStr,
    thread::sleep
};
use tokio::io::{self, AsyncWriteExt};

#[derive(Clap)]
#[clap(version = "1.0", author = "Ryza <[email protected]>")]
pub struct Opts {
    /// Sets a custom config file.
    #[clap(short, long, default_value = "twitter-img-saver.json")]
    pub config: String,
}

#[derive(Deserialize, Clone)]
struct Config {
    /// Twitter Consumer API Key
    key: String,
    /// Twitter Consumer API Secret
    secret: String,
    /// Path to the directory where you'd like to save images
    img_dir: String,
    /// Twitter Usernames. Their tweets with images will be saved.
    users: Vec<String>
}

impl Config {
    pub fn load<P>(path: P) -> io::Result<Config> where P: AsRef<Path> {
        // Open the file in read-only mode with buffer.
        let reader = BufReader::new(File::open(path)?);
        let config = serde_json::from_reader(reader)?;
        Ok(config)
    }
}

struct TwitterUser {
    image: HashMap<u64, bool>,
    timeline: Timeline,
}

impl TwitterUser {
    fn new(timeline: Timeline) -> TwitterUser { TwitterUser{ image: HashMap::new(), timeline } }
}

#[tokio::main]
#[allow(unreachable_code)]
async fn main() -> egg_mode::error::Result<()> {
    // parse cli args
    let config_path = Opts::parse().config;
    // load config
    let config = Config::load(&config_path)
        .expect("[ERROR] cannot parse config file");
    // generate keypair and get bearer token
    let token = egg_mode::KeyPair::new(config.key.clone(), config.secret.clone());
    println!("[INFO] Pulling up the bearer token...");
    let token = egg_mode::auth::bearer_token(&token).await?;

    // user list, their tweets with images will be saved
    let mut users = config.users.clone();
    // @username, photo_id, is_saved
    let mut mem_record : HashMap<String, TwitterUser> = HashMap::new();
    // setup according to user list
    set_user_record(&token, users, &mut mem_record).await;
    loop {
        // save images now
        save_images(&mut mem_record, &config.img_dir).await?;
        // wait for 600 seconds
        sleep(std::time::Duration::from_secs(600));
        // try to reload user list from config file
        // (so that we don't need to restart the whole program)
        users = Config::load(&config_path).unwrap_or(config.clone()).users;
        // update memory record based on user list
        set_user_record(&token, users, &mut mem_record).await;
    }
    Ok(())
}

/// Setup user records
///
/// @param token      Twitter bearer token
/// @param users      List of usernames
/// @param mem_record Lookup table for checking whether if an image is saved
async fn set_user_record(token: &Token, users: Vec<String>, mem_record: &mut HashMap<String, TwitterUser>) {
    // get all active usernames
    let mut existed = mem_record.keys().map(|s| (s.clone(), false)).collect::<HashMap<String, bool>>();
    // for each username in the given list
    for username in &users {
        // initialize corresponding timeline if not in the record
        if !mem_record.contains_key(username) {
            mem_record.insert(username.clone(), initialize_timeline(username, &token).await);
        } else {
            // otherwise it's still active
            *existed.get_mut(username).unwrap() = true;
        }
    }
    // for usernames not appearing in the new list
    let _ = existed.iter().map(|(username, enabled)| {
        if !enabled {
            // remove from the record
            mem_record.remove(username);
        }
    }).collect::<Vec<_>>();
}

/// Initialize timeline of a user
async fn initialize_timeline(username: &String, token: &Token) -> TwitterUser {
    // 50 recent tweets (then filter out all replies and retweets)
    let timeline = egg_mode::tweet::user_timeline(username.to_string(), false, false, &token)
        .with_page_size(50);
    TwitterUser::new(timeline)
}

/// Save Twitter Images
///
/// @param mem_record Lookup table for checking whether if an image is saved
/// @return OK(()) or any Err(e) occurred during pulling the timeline or downloading images
async fn save_images(mem_record: &mut HashMap<String, TwitterUser>, img_dir: &String) -> egg_mode::error::Result<()> {
    // try to pull each users' timeline
    for (username, user) in mem_record.iter_mut() {
        let timeline = &user.timeline;
        println!("[INFO] try to fetch timeline of @{}", username);
        let latest_id = timeline.max_id;
        let feed = timeline.call(latest_id, None).await?;
        for tweet in feed.response {
            match extract_from_stream(&tweet, user, img_dir).await {
                Err(e) => println!("[ERROR] {}", e.to_string()),
                Ok(num_image_saved) => match num_image_saved {
                    0 => println!("[INFO] no new image from @{}", username),
                    _ => println!("[INFO] {} new images saved from @{}", num_image_saved, username),
                },
            }
        }
    }
    Ok(())
}

/// Extract media from a single tweet
async fn extract_from_stream(tweet: &Tweet, user_record: &mut TwitterUser, img_dir: &String) -> std::result::Result<u32, Box<dyn std::error::Error + Send + Sync>> {
    let mut num_image_saved = 0u32;
    if let Some(ref user) = tweet.user {
        let username = user.screen_name.clone();
        if let Some(ref media) = tweet.extended_entities {
            let mut media_is_photo = false;
            for info in &media.media {
                match info.media_type {
                    MediaType::Photo => {
                        if !media_is_photo {
                            println!("[INFO] tweet with image from @{}", username);
                            media_is_photo = true;
                        }
                        if !*user_record.image.get(&info.id).unwrap_or(&false) {
                            num_image_saved += fetch_url(
                                hyper::Uri::from_str(format!("{}?format=png&name=large", info.media_url_https).as_str()).unwrap(),
                                format!("{}/{}", img_dir, username),
                                format!("{}.png", info.id)).await?;
                            user_record.image.insert(info.id.clone(), true);
                        }
                    },
                    _ => (),
                }
            }
        }
    }
    Ok(num_image_saved)
}

/// Fetch URL and save file to disk
async fn fetch_url(url: hyper::Uri, save_dir: String, save_as: String) -> std::result::Result<u32, Box<dyn std::error::Error + Send + Sync>> {
    tokio::fs::create_dir_all(&save_dir).await?;
    let filepath = format!("{}/{}", save_dir, save_as);
    if !Path::new(&filepath).exists() {
        let client = Client::builder().build::<_, hyper::Body>(HttpsConnector::new());
        let mut res = client.get(url.clone()).await?;
        match res.status().as_u16() {
            200 => {
                let mut file = tokio::fs::File::create(&filepath).await?;
                while let Some(next) = res.data().await {
                    file.write_all(&next?).await?;
                }
            },
            other => eprintln!("[ERROR] HTTP {} returned for {}", other, url.to_string()),
        }
        Ok(1)
    } else {
        Ok(0)
    }
}