Skip to content

Commit

Permalink
chore: update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
werifu committed Nov 23, 2022
1 parent e81abeb commit d560e99
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 37 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ Cargo.lock
**/*.rs.bk

pic/
config.toml
config.toml
tweet-like-listener
8 changes: 0 additions & 8 deletions README-ZH.md

This file was deleted.

19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
# tweet-like-listener
# tweet-like-listener (推特点赞图片下载)

Download your 'like' images on twitter automatically.
号养好了,每天上推特都是色图,又懒得手动右键下载图片,那就把这项工作自动化吧!

## 功能
* 指定账号轮询,下载其近期点赞的推特图片到本地
* 多账号支持,群 友 严 选

## 使用
1. **important** 需要[推特开发者账号](https://developer.twitter.com/en/portal/petition/essential/basic-info),请在里面申请并创建一个应用,得到 access_key(这是你访问推特 API 的凭证)
2. **important** 自行解决科学上网问题
3. release 下载对应平台应用(也可以本地 cargo 编译)
4. 执行应用(第一次执行会生成 config.toml 文件,如果没有请在相同目录下新建)
5. 填写 config.toml 文件
6. 再次执行应用,并使用任意方式让其能一直跑下去

## What you need

* a twitter developer account
* a server / computer that can reach twitter
45 changes: 40 additions & 5 deletions src/downloader.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use std::collections::HashMap;
use std::process::exit;
use std::time::Duration;

use hyper::http::HeaderValue;
use hyper::{HeaderMap, StatusCode};

use crate::model::{Attachments, Media, Tweet, TweetResp, User, UserResp};
use crate::url::UrlBuilder;
use crate::Result;
use log::{error, debug};
use log::{debug, error};

pub const TIMEOUT: Duration = Duration::from_secs(6);

pub struct Downloader {
pub user_cache: HashMap<String, User>,
pub user_ids: Vec<String>,
Expand Down Expand Up @@ -48,7 +52,12 @@ impl Downloader {
.get_url();
let headers = self.tweet_auth_header();

let resp = client.get(url).headers(headers).send().await?;
let resp = client
.get(url)
.headers(headers)
.timeout(TIMEOUT)
.send()
.await?;
if resp.status() == StatusCode::UNAUTHORIZED {
error!("401: Maybe you have a wrong access_key");
exit(1);
Expand Down Expand Up @@ -127,8 +136,21 @@ impl Downloader {
error!("401: Maybe you have a wrong access_key");
exit(1);
}

let resp = resp.json::<UserResp>().await?;
Ok(resp.data)
if let Some(errs) = resp.errors {
error!(
"{}",
errs.iter()
.map(|err| err.detail.clone())
.collect::<Vec<String>>()
.join(";")
);
}
if let Some(users) = resp.data {
return Ok(users);
}
Ok(vec![])
}

pub async fn get_users_by_usernames(&self, usernames: Vec<&str>) -> Result<Vec<User>> {
Expand All @@ -138,14 +160,27 @@ impl Downloader {
let resp = client
.get(url)
.headers(self.tweet_auth_header())
.timeout(TIMEOUT)
.send()
.await?;
if resp.status() == StatusCode::UNAUTHORIZED {
error!("401: Maybe you have a wrong access_key");
error!("401: Maybe you have a wrong access_key, please check your config.toml");
exit(1);
}
let resp = resp.json::<UserResp>().await?;
Ok(resp.data)
if let Some(errs) = resp.errors {
error!(
"{}",
errs.iter()
.map(|err| err.detail.clone())
.collect::<Vec<String>>()
.join(";")
);
}
if let Some(users) = resp.data {
return Ok(users);
}
Ok(vec![])
}
}

Expand Down
58 changes: 42 additions & 16 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use downloader::Downloader;
use downloader::{Downloader, TIMEOUT};

pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

Expand All @@ -11,7 +11,7 @@ use std::{
process::exit,
time::Duration,
};
use tokio::time::sleep; // 1.3.1
use tokio::{task, time::sleep}; // 1.3.1

mod downloader;
mod model;
Expand Down Expand Up @@ -40,20 +40,35 @@ const CONFIG_CONTENT: &str = r#"
# access_key of your application
access_key = 'your twitter access_key'
# usernames you would like to listen
# usernames you would like to listen, with prefix '@'
usernames = ['@werifu_']
# request frequency. unit: 1 second
freq = 5
[storage]
# the dir that stores images
dir = './pic'
"#;

#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();

// check the network
tokio::spawn(async {
loop {
let res = reqwest::Client::new().get("https://twitter.com").timeout(TIMEOUT).send().await;
match res {
Ok(_) => {
info!("ping Twitter ok");
},
Err(_) => {
error!("ping Twitter failed. You may not reach twitter. (tips tor China Mainland users: check your proxy)");
}
};
sleep(Duration::from_secs(10)).await;
}
});
match fs::read_to_string("./config.toml") {
Ok(config_str) => {
let config: Config = toml::from_str(&config_str).unwrap();
Expand All @@ -72,11 +87,12 @@ async fn main() -> Result<()> {
let mut downloader = Downloader::new(config.twitter.access_key);
let users = match downloader.get_users_by_usernames(usernames).await {
Ok(users) => users,
Err(err) => {
error!("usernames maybe wrong or you may not reach twitter.\nPlease check your config and net.\nerr: {:#?}", err);
Err(_) => {
error!("You may not reach twitter. Please check your config and net. (tips tor China Mainland users: check your proxy)");
exit(1);
}
};
info!("ok users: {:?}", users);

loop {
for user in users.iter() {
Expand All @@ -87,21 +103,31 @@ async fn main() -> Result<()> {
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
likes.len()
);
let mut handles = vec![];
for (filename, url) in likes.iter() {
let full_path = dir.join(filename);
let filename = filename.clone();
let url = url.clone();
let full_path = dir.join(&filename);
if full_path.exists() {
continue;
}
match reqwest::Client::new().get(url).send().await {
Ok(img_bytes) => {
let img_bytes = img_bytes.bytes().await.unwrap();
let mut f = File::create(full_path).unwrap();
f.write(&img_bytes).unwrap();
// download pictures concurrently
handles.push(task::spawn(async move {
match reqwest::Client::new().get(url).send().await {
Ok(img_bytes) => {
let img_bytes = img_bytes.bytes().await.unwrap();
let mut f = File::create(full_path).unwrap();
f.write(&img_bytes).unwrap();
}
Err(err) => {
error!("download file {} error: {:?}", filename, err);
}
}
Err(err) => {
error!("download file error: {:?}", err);
}
}
}));
}
// await for all download tasks
for handle in handles {
let _ = handle.await;
}
}
Err(err) => {
Expand Down
16 changes: 14 additions & 2 deletions src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,17 @@ pub struct TweetResp {

#[derive(Serialize, Deserialize, Debug)]
pub struct UserResp {
pub data: Vec<User>,
}
pub data: Option<Vec<User>>,
pub errors: Option<Vec<UserErr>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UserErr {
pub value: String,
pub detail: String,
pub title: String,
pub resource_type: String,
pub parameter: String,
pub resource_id: String,
pub r#type: String,
}

0 comments on commit d560e99

Please sign in to comment.