From 3de6101242d0a96040e874f1219623794fa4455f Mon Sep 17 00:00:00 2001 From: Douman Date: Sat, 15 Jun 2019 11:38:18 +0300 Subject: [PATCH] 0.14.1: Add fie auth command This command will allow user to authorize with twitter using PIN based token --- Cargo.toml | 2 +- README.md | 47 +++++++++--- appveyor.yml | 4 +- docs/configuration.md | 32 +++++++-- fie.toml | 5 +- src/cli/auth.rs | 140 ++++++++++++++++++++++++++++++++++++ src/cli/cli.rs | 10 +++ src/cli/main.rs | 25 +++++++ src/lib/api/http.rs | 13 ++-- src/lib/api/mod.rs | 2 +- src/lib/api/twitter/data.rs | 6 ++ src/lib/config.rs | 2 + 12 files changed, 262 insertions(+), 26 deletions(-) create mode 100644 src/cli/auth.rs diff --git a/Cargo.toml b/Cargo.toml index e2963ac..280b3c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fie" -version = "0.14.0" +version = "0.14.1" authors = ["Douman "] repository = "https://github.com/DoumanAsh/fie" description = "Small and cute social media utility." diff --git a/README.md b/README.md index a565846..9e4136c 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,29 @@ [![Build status](https://ci.appveyor.com/api/projects/status/oc937oppd38x1y4y/branch/master?svg=true)](https://ci.appveyor.com/project/DoumanAsh/fie/branch/master) [![Build Status](https://travis-ci.org/DoumanAsh/fie.svg?branch=master)](https://travis-ci.org/DoumanAsh/fie) [![Crates.io](https://img.shields.io/crates/v/fie.svg)](https://crates.io/crates/fie) -[![Dependency status](https://deps.rs/crate/fie/0.14.0/status.svg)](https://deps.rs/crate/fie) +[![Dependency status](https://deps.rs/crate/fie/0.14.1/status.svg)](https://deps.rs/crate/fie) Small and cute social media CLI. ![Icon](icon.jpg) -## Download links +## Installation -* Windows [32bit](https://github.com/DoumanAsh/fie/releases/download/0.14.0/fie-0.14.0-i686-pc-windows-msvc.zip) -* Windows [64bit](https://github.com/DoumanAsh/fie/releases/download/0.14.0/fie-0.14.0-x86_64-pc-windows-msvc.zip) -* Linux [64bit](https://github.com/DoumanAsh/fie/releases/download/0.14.0/fie-0.14.0-x86_64-unknown-linux-gnu.zip) -* OSX [64bit](https://github.com/DoumanAsh/fie/releases/download/0.14.0/fie-0.14.0-x86_64-apple-darwin.zip) +### Download links + +* Windows [32bit](https://github.com/DoumanAsh/fie/releases/download/0.14.1/fie-0.14.1-i686-pc-windows-msvc.zip) +* Windows [64bit](https://github.com/DoumanAsh/fie/releases/download/0.14.1/fie-0.14.1-x86_64-pc-windows-msvc.zip) +* Linux [64bit](https://github.com/DoumanAsh/fie/releases/download/0.14.1/fie-0.14.1-x86_64-unknown-linux-gnu.zip) +* OSX [64bit](https://github.com/DoumanAsh/fie/releases/download/0.14.1/fie-0.14.1-x86_64-apple-darwin.zip) + +### Cargo + +In order to install CLI utility you need to enable feature `cli` +In addition to that following environment variables are used optionally: + +- Twitter Consumer Token (requires both to present for it to be used): + - `FIE_TWITTER_CONSUMER_KEY` - Builtin Consumer key for twitter API; + - `FIE_TWITTER_CONSUMER_SECRET` - Builtin Consumer secret for twitter API; ## Supported social platforms: @@ -48,6 +59,7 @@ FLAGS: -V, --version Prints version information SUBCOMMANDS: + auth Allows to perform authorization with social media. batch Load CLI arguments from file and runs it. env Prints information about app environment. help Prints this message or the help of the given subcommand(s) @@ -79,9 +91,9 @@ ARGS: ### batch -``` Load CLI arguments from file and runs it. +``` USAGE: fie.exe batch @@ -100,8 +112,6 @@ File examples: Prints information about app's environment. ``` -Prints information about app environment. - USAGE: fie.exe env @@ -112,3 +122,22 @@ SUBCOMMANDS: config Prints path to config file. help Prints this message or the help of the given subcommand(s) ``` + +### auth + +Allows to perform user authorization using social media API. +Currently available authorizations: + +- Twitter PIN based auth. Interactive dialogue will prompt you to follow link and authorize fie. + +``` +USAGE: + fie.exe auth + +FLAGS: + -h, --help Prints help information + +SUBCOMMANDS: + help Prints this message or the help of the given subcommand(s) + twitter Performs authorization with twitter +``` diff --git a/appveyor.yml b/appveyor.yml index 86bb84b..0ca28cf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,10 +2,10 @@ environment: global: PROJECT_NAME: fie matrix: - - TARGET: i686-pc-windows-msvc - CHANNEL: stable - TARGET: x86_64-pc-windows-msvc CHANNEL: stable + - TARGET: i686-pc-windows-msvc + CHANNEL: stable # Install Rust and Cargo # (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml) diff --git a/docs/configuration.md b/docs/configuration.md index 3d7f97f..c552c58 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -32,13 +32,6 @@ password = "password" ## Twitter -Go to [app page](https://apps.twitter.com/) and create new app for yourself. - -After that go to section `Keys and Access Tokens` to retrieve configuration: - -`Application Settings` has `Consumer Key` and `Consumer Secret` -Put it in section below: - ```toml [api.twitter.consumer] key = "key" @@ -54,6 +47,31 @@ key = "token" secret = "secret" ``` +### Using own application + +Go to [app page](https://apps.twitter.com/) and create new app for yourself. + +After that go to section `Keys and Access Tokens` to retrieve configuration: + +`Application Settings` has `Consumer Key` and `Consumer Secret` +Put it in section below: + +### Using fie builtin tokens + +In this case fie must have been built with following environment variables: + +- `FIE_TWITTER_CONSUMER_KEY` - contains consumer key of application. +- `FIE_TWITTER_CONSUMER_SECRET` - contains consumer secret of application. + +Provided download links will contain `fie` own consumer token. +Therefore `api.twitter.consumer` can be omitted + +In this case you can use command `fie auth twitter` in order to get `api.twitter.access` +After successfully following interactive instructions, the `api.twitter.access` configuration will be printed in stdout. +This should replace whatever you already have in your configuration. + +Use `fie env config` to find configuration file location. + ## Mastodon You need to provide host name of the Mastodon instance. diff --git a/fie.toml b/fie.toml index 8f1dc59..684fae0 100644 --- a/fie.toml +++ b/fie.toml @@ -9,9 +9,10 @@ username = "username" password = "password" # Consumer Token of twitter app +# This can be omitted to use builtin consumer token with authorization command [api.twitter.consumer] -key = "key" -secret = "secret" +key = "" +secret = "" # Authorization Token to access user account. Generated in developer page. [api.twitter.access] diff --git a/src/cli/auth.rs b/src/cli/auth.rs new file mode 100644 index 0000000..fa5089d --- /dev/null +++ b/src/cli/auth.rs @@ -0,0 +1,140 @@ +use fie::config; +use fie::api; +use fie::api::http::{self, AutoClient, AutoRuntime, Request}; +use serde_derive::{Deserialize}; + +use std::io::{self, Write}; +use std::collections::HashMap; + +pub fn twitter(mut config: config::Twitter) { + const REQUEST_TOKEN_URI: &str = "https://api.twitter.com/oauth/request_token"; + const ACCESS_TOKEN_URI: &str = "https://api.twitter.com/oauth/access_token"; + + #[derive(Deserialize, Debug)] + struct RequestTokenRsp { + oauth_token: String, + oauth_token_secret: String, + } + + config.access.key.truncate(0); + config.access.secret.truncate(0); + + let mut oauth = api::twitter::data::Oauth::new(config); + + let _http = http::init(&Default::default()); + + let (auth_params, auth_header) = { + let mut auth_params = HashMap::new(); + auth_params.insert("oauth_callback", "oob"); + auth_params.insert("x_auth_access_type", "write"); + (auth_params.clone(), oauth.gen_auth(&http::Method::POST, REQUEST_TOKEN_URI, auth_params)) + }; + + let req = Request::post(REQUEST_TOKEN_URI).expect("To create request") + .set_header(http::header::AUTHORIZATION, auth_header) + .form(&auth_params) + .expect("To serialize form params") + .send() + .finish(); + + let request_token: RequestTokenRsp = match req { + Ok(response) => match response.is_success() { + true => match response.text().finish() { + Ok(response) => match yukikaze::serde_urlencoded::from_str(&response) { + Ok(response) => response, + Err(error) => { + eprintln!("Unable to parse response with request token. Error: {}", error); + return; + } + }, + Err(error) => { + eprintln!("Failed to read response with requested token. Error: {}", error); + return; + } + }, + false => { + eprintln!("Request for token failed with {}", response.status()); + return; + } + }, + Err(error) => { + eprintln!("Failed to request ouath token :( Error: {}", error); + return; + } + }; + + println!("Please use following link to authroize fie:\nhttps://api.twitter.com/oauth/authorize?oauth_token={}", request_token.oauth_token); + println!("Once done please enter PIN..."); + let pin = { + let mut buffer = String::new(); + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + loop { + buffer.truncate(0); + + let _ = stdout.write_all(b"Print: "); + let _ = stdout.flush(); + match stdin.read_line(&mut buffer) { + Ok(_) => (), + Err(_) => { + let _ = stdout.write_all(b"Failed to read input. Try again...\n"); + continue; + } + } + + let pin = buffer.trim(); + match pin.parse::() { + Ok(_) => break pin.to_owned(), + Err(_) => { + let _ = stdout.write_all(b"Invalid PIN specified, should contain only digits. Try again...\n"); + continue; + } + } + } + }; + + oauth.set_oauth_token(&request_token.oauth_token); + let (auth_params, auth_header) = { + let mut auth_params = HashMap::new(); + auth_params.insert("oauth_verifier", pin.trim()); + (auth_params.clone(), oauth.gen_auth(&http::Method::POST, ACCESS_TOKEN_URI, auth_params)) + }; + + let req = Request::post(ACCESS_TOKEN_URI).expect("To create request") + .set_header(http::header::AUTHORIZATION, auth_header) + .form(&auth_params) + .expect("To serialize form params") + .send() + .finish(); + + let access_token: RequestTokenRsp = match req { + Ok(response) => match response.is_success() { + true => match response.text().finish() { + Ok(response) => match yukikaze::serde_urlencoded::from_str(&response) { + Ok(response) => response, + Err(error) => { + eprintln!("Unable to parse response with access token. Error: {}", error); + return; + } + }, + Err(error) => { + eprintln!("Failed to read response with access token. Error: {}", error); + return; + } + }, + false => { + eprintln!("Request for access token failed with {}", response.status()); + return; + } + }, + Err(error) => { + eprintln!("Failed to request access token :( Error: {}", error); + return; + } + }; + + println!("Received access token successfully.\nAdd following to your fie configuration file:"); + println!("[api.twitter.access]\nkey = \"{}\"\nsecret = \"{}\"", access_token.oauth_token, access_token.oauth_token_secret); +} diff --git a/src/cli/cli.rs b/src/cli/cli.rs index d1ff394..83933d2 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -54,6 +54,9 @@ pub enum Command { #[structopt(name = "batch")] ///Load CLI arguments from file and runs it. Batch(Batch), + #[structopt(name = "auth")] + ///Allows to perform authorization with social media. + Auth(Auth), } #[derive(Debug, StructOpt)] @@ -98,3 +101,10 @@ pub enum Env { ///Prints path to config file. Config, } + +#[derive(Debug, StructOpt)] +pub enum Auth { + #[structopt(name = "twitter")] + ///Performs authorization with twitter + Twitter, +} diff --git a/src/cli/main.rs b/src/cli/main.rs index f3870fb..8b41d01 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -4,6 +4,7 @@ use serde_derive::Deserialize; mod config; mod cli; +mod auth; use fie::config::Config; use config::FileSystemLoad; @@ -78,8 +79,27 @@ fn open_batch(path: &str) -> io::Result { config::load_from_file(Path::new(path)) } +fn use_twitter_builtin_consumer(twitter: &mut fie::config::Twitter) { + const CONSUMER_KEY: Option<&'static str> = option_env!("FIE_TWITTER_CONSUMER_KEY"); + const CONSUMER_SECRET: Option<&'static str> = option_env!("FIE_TWITTER_CONSUMER_SECRET"); + + //Only set if either part of consumer token is missing + match (CONSUMER_KEY, CONSUMER_SECRET) { + (Some(key), Some(secret)) => if twitter.consumer.key.len() == 0 || twitter.consumer.secret.len() == 0{ + twitter.consumer.key.truncate(0); + twitter.consumer.secret.truncate(0); + + twitter.consumer.key.push_str(key); + twitter.consumer.secret.push_str(secret); + }, + _ => (), + } +} + fn run() -> io::Result<()> { let mut config = Config::load()?; + use_twitter_builtin_consumer(&mut config.api.twitter); + let args = cli::Args::new(&mut config.platforms); match args.cmd { @@ -100,6 +120,11 @@ fn run() -> io::Result<()> { }, cli::Command::Env(env) => match env { cli::Env::Config => println!("{}", Config::path()?.display()) + }, + cli::Command::Auth(typ) => match typ { + cli::Auth::Twitter => { + auth::twitter(config.api.twitter); + } } } diff --git a/src/lib/api/http.rs b/src/lib/api/http.rs index 9ba7c8f..3b6c906 100644 --- a/src/lib/api/http.rs +++ b/src/lib/api/http.rs @@ -1,3 +1,5 @@ +//!Http runtime + use yukikaze::client::config::Config; pub use yukikaze::client::request::multipart; pub use yukikaze::client::request::Builder; @@ -23,22 +25,25 @@ impl Config for Conf { } } +///Http runtime used by fie pub struct HttpRuntime { - pub tokio: tokio_global::single::Runtime, - pub http: GlobalClient, + _tokio: tokio_global::single::Runtime, + _http: GlobalClient, } +///Initializes instance of runtime pub fn init(settings: &Settings) -> HttpRuntime { unsafe { TIMEOUT = settings.timeout; } HttpRuntime { - tokio: tokio_global::single::init(), - http: GlobalClient::with_config::(), + _tokio: tokio_global::single::init(), + _http: GlobalClient::with_config::(), } } +///Gets currently set timeout value pub fn get_timeout() -> Duration { unsafe { Duration::from_secs(TIMEOUT) } } diff --git a/src/lib/api/mod.rs b/src/lib/api/mod.rs index c06a3bb..8344036 100644 --- a/src/lib/api/mod.rs +++ b/src/lib/api/mod.rs @@ -1,6 +1,6 @@ //!Social medias API module -mod http; +pub mod http; pub mod twitter; pub mod gab; pub mod mastodon; diff --git a/src/lib/api/twitter/data.rs b/src/lib/api/twitter/data.rs index ac09a52..0ad06d6 100644 --- a/src/lib/api/twitter/data.rs +++ b/src/lib/api/twitter/data.rs @@ -67,6 +67,12 @@ impl Oauth { } } + ///Sets new value oauth token. Must be percent encoded. + pub fn set_oauth_token(&mut self, new_token: &str) { + self.oauth_token.truncate(0); + self.oauth_token.push_str(new_token); + } + /// Returns Authorization header value /// /// The important thing here is signature which is diff --git a/src/lib/config.rs b/src/lib/config.rs index f372135..8ca9731 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -47,8 +47,10 @@ pub struct Token { /// Twitter configuration #[derive(Serialize, Deserialize, Debug, Default)] pub struct Twitter { + #[serde(default)] ///Consumer tokens, belongs to app. pub consumer: Token, + #[serde(default)] ///Access tokens, granted per user. pub access: Token, }