Skip to content

Commit

Permalink
Allow to include images in posts. For now only 1 image
Browse files Browse the repository at this point in the history
  • Loading branch information
DoumanAsh committed Oct 22, 2017
1 parent 04c7297 commit 598ac10
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 34 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ serde_json = "1"
toml = "0"
serde_derive = "1"

egg-mode = "0.*"
egg-mode = { git = "https://github.com/QuietMisdreavus/twitter-rs" }
tokio-core = "0.*"
futures = "0.*"
hyper = "0.*"
hyper-tls = "0.*"
mime_guess = "2.0.0-alpha.2"
73 changes: 63 additions & 10 deletions src/api/gab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,33 @@ mod hyper {
pub use ::hyper_tls::{HttpsConnector};
}

use ::hyper::mime;

use ::serde_json;
use ::tokio_core::reactor::{
Handle
};

use super::common;
use ::config;
use ::utils::Image;

const POST_URL: &'static str = "https://gab.ai/posts";
const IMAGES_URL: &'static str = "https://gab.ai/api/media-attachments/images";

mod payload {
pub mod payload {
#[derive(Serialize, Debug)]
pub struct Post {
body: String,
reply_to: String,
is_quote: u8,
gif: String,
category: Option<String>,
topic: Option<String>,
share_twitter: bool,
share_facebook: bool,
is_replies_disabled: bool
pub reply_to: String,
pub is_quote: u8,
pub gif: String,
pub category: Option<String>,
pub topic: Option<String>,
pub share_twitter: bool,
pub share_facebook: bool,
pub is_replies_disabled: bool,
pub media_attachments: Vec<String>
}

impl Post {
Expand All @@ -40,10 +45,16 @@ mod payload {
topic: None,
share_twitter: false,
share_facebook: false,
is_replies_disabled: false
is_replies_disabled: false,
media_attachments: Vec::new()
}
}
}

#[derive(Deserialize, Debug)]
pub struct UploadResponse {
pub id: String
}
}

///Gab.ai Client
Expand Down Expand Up @@ -71,6 +82,34 @@ impl Client {
})
}

fn multipart_mime() -> mime::Mime {
"multipart/form-data; boundary=-fie".parse().unwrap()
}

fn multipart_body(image: &Image) -> (Vec<u8>, u64) {
let mut body = Vec::with_capacity(image.content.len());
body.extend("\r\n---fie\r\n".as_bytes().iter());
body.extend(format!("Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n", image.name).as_bytes().iter());
body.extend(format!("Content-Type: {}\r\n\r\n", image.mime).as_bytes().iter());
body.extend(image.content.iter());
body.extend("\r\n---fie--\r\n".as_bytes().iter());
let len = body.len() as u64;
(body, len)
}

///Uploads image to gab.ai.
pub fn upload_image(&self, image: &Image) -> hyper::FutureResponse {
let mut req = hyper::Request::new(hyper::Method::Post, IMAGES_URL.parse().unwrap());
req.headers_mut().set(hyper::ContentType(Self::multipart_mime()));
req.headers_mut().set(self.auth());

let (payload, len) = Self::multipart_body(image);
req.headers_mut().set(hyper::ContentLength(len));
req.set_body(payload);

self.hyper.request(req)
}

///Post new message.
pub fn post(&self, message: &str, tags: &Option<Vec<String>>) -> hyper::FutureResponse {
let message = common::message(message, tags);
Expand All @@ -84,6 +123,20 @@ impl Client {
self.hyper.request(req)
}

///Posts new message with image
pub fn post_w_images(&self, message: &str, tags: &Option<Vec<String>>, images: &[String]) -> hyper::FutureResponse {
let message = common::message(message, tags);
let mut message = payload::Post::new(message);
message.media_attachments.extend(images.iter().cloned());

let mut req = hyper::Request::new(hyper::Method::Post, POST_URL.parse().unwrap());
req.headers_mut().set(hyper::ContentType::json());
req.headers_mut().set(self.auth());
req.set_body(serde_json::to_string(&message).unwrap());

self.hyper.request(req)
}

pub fn handle_post(response: hyper::Response) -> Result<(), String> {
if response.status() != hyper::StatusCode::Ok {
return Err(format!("Failed to post. Status: {}", response.status()));
Expand Down
15 changes: 15 additions & 0 deletions src/api/twitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use ::egg_mode::{
FutureResponse,
Response,
tweet,
media
};

use ::tokio_core::reactor::{
Expand All @@ -14,6 +15,8 @@ use ::tokio_core::reactor::{
use super::common;
use ::config;

use ::utils::Image;

///Twitter client.
pub struct Client {
///Twitter access token.
Expand All @@ -36,13 +39,25 @@ impl Client {
}
}

///Uploads image to twitter.
pub fn upload_image(&self, image: &Image) -> FutureResponse<media::Media> {
media::upload_image(&image.content, &self.token, &self.handle)
}

///Posts new tweet.
pub fn post(&self, message: &str, tags: &Option<Vec<String>>) -> FutureResponse<tweet::Tweet> {
let message = common::message(message, tags);

tweet::DraftTweet::new(&message).send(&self.token, &self.handle)
}

///Posts new tweet with images.
pub fn post_w_images(&self, message: &str, tags: &Option<Vec<String>>, images: &[u64]) -> FutureResponse<tweet::Tweet> {
let message = common::message(message, tags);

tweet::DraftTweet::new(&message).media_ids(images).send(&self.token, &self.handle)
}

pub fn handle_post(response: Response<tweet::Tweet>) -> Result<(), String> {
Ok(println!("OK(id={})", response.response.id))
}
Expand Down
24 changes: 15 additions & 9 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ fn new_command() -> App<'static, 'static> {
.takes_value(true)
.number_of_values(1)
.multiple(true))
.arg(arg("image").short("i")
.takes_value(true))
}

pub fn parser() -> App<'static, 'static> {
Expand All @@ -49,10 +51,16 @@ pub fn parser() -> App<'static, 'static> {
}

#[derive(Debug)]
///Command representation with all its arguments
///Command representation with all its arguments.
pub enum Commands {
///Creates new tweet
Post(String, Option<Vec<String>>)
///Creates new tweet.
///
///# Parameters:
///
///* First - Text.
///* Second - Tags.
///* Third - Image to attach.
Post(String, Option<Vec<String>>, Option<String>)
}

impl Commands {
Expand All @@ -63,12 +71,10 @@ impl Commands {
match name {
"post" => {
let message = matches.value_of("message").unwrap().to_string();
if let Some(tags) = matches.values_of("tag") {
Commands::Post(message, Some(tags.map(|tag| format!("#{}", tag)).collect()))
}
else {
Commands::Post(message, None)
}
let tags = matches.values_of("tag").map(|values| values.map(|tag| format!("#{}", tag)).collect());
let image = matches.value_of("image").map(|image| image.to_string());

Commands::Post(message, tags, image)
},
_ => unimplemented!()
}
Expand Down
35 changes: 21 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,49 @@ extern crate tokio_core;
extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate mime_guess;

use futures::future::Future;
use futures::Stream;
use tokio_core::reactor::Core;

use std::env;
use std::path;

mod api;
mod cli;
#[macro_use]
mod utils;
mod config;

fn get_config() -> path::PathBuf {
let mut result = env::current_exe().unwrap();

result.set_file_name(config::NAME);

result
}

fn run() -> Result<i32, String> {
let config = config::Config::from_file(&get_config())?;
let config = config::Config::from_file(&utils::get_config())?;
let args = cli::Args::new()?;

let mut tokio_core = Core::new().map_err(error_formatter!("Unable to create tokios' event loop."))?;
let twitter = api::twitter::Client::new(tokio_core.handle(), config.twitter);
let gab = api::gab::Client::new(tokio_core.handle(), config.gab);

match args.command {
cli::Commands::Post(message, tags) => {
cli::Commands::Post(message, tags, None) => {
println!(">>>Gab:");
tokio_core.run(gab.post(&message, &tags).map_err(error_formatter!("Cannot post.")).and_then(api::gab::Client::handle_post))?;
println!(">>>Twitter:");
tokio_core.run(twitter.post(&message, &tags).map_err(error_formatter!("Cannot tweet.")).and_then(api::twitter::Client::handle_post))?
tokio_core.run(twitter.post(&message, &tags).map_err(error_formatter!("Cannot tweet.")).and_then(api::twitter::Client::handle_post))?;
},
cli::Commands::Post(message, tags, Some(image)) => {
let image = utils::open_image(image).map_err(error_formatter!("Cannot open image!"))?;
println!(">>>Gab:");
let gab_post = gab.upload_image(&image).map_err(error_formatter!("Cannot upload image."))
.and_then(handle_bad_hyper_response!("Cannot upload image."))
.and_then(|response| response.body().concat2().map_err(error_formatter!("Cannot read image upload's response")))
.and_then(move |body| serde_json::from_slice(&body).map_err(error_formatter!("Cannot parse image upload's response")))
.and_then(|response: api::gab::payload::UploadResponse| gab.post_w_images(&message, &tags, &[response.id]).map_err(error_formatter!("Cannot post.")))
.and_then(api::gab::Client::handle_post);
tokio_core.run(gab_post)?;
println!(">>>Twitter:");
let tweet = twitter.upload_image(&image).map_err(error_formatter!("Cannot upload image."))
.and_then(|rsp| twitter.post_w_images(&message, &tags, &[rsp.response.id]).map_err(error_formatter!("Cannot tweet.")))
.and_then(api::twitter::Client::handle_post);
tokio_core.run(tweet)?;
}
}

Ok(0)
Expand Down
61 changes: 61 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,66 @@
use ::std::env;
use ::std::fs::{
File
};
use std::path::{
Path,
PathBuf
};
use ::std::io;
use self::io::{
BufReader,
Read
};

use ::mime_guess::{
Mime,
guess_mime_type
};

#[macro_export]
macro_rules! error_formatter {
($prefix:expr) => { |error| format!("{} Error: {}", $prefix, error) }
}

#[macro_export]
macro_rules! handle_bad_hyper_response {
($prefix:expr) => { |response| match response.status() {
hyper::StatusCode::Ok => Ok(response),
_ => Err(format!("{} Bad response. Status: {}", $prefix, response.status()))
}}
}

use ::config;

pub struct Image {
pub name: String,
pub mime: Mime,
pub content: Vec<u8>
}

///Opens image file and returns its content.
pub fn open_image<P: AsRef<Path>>(path: P) -> io::Result<Image> {
let file = File::open(&path)?;
let file_len = file.metadata()?.len();
let mut file = BufReader::new(file);

let name = path.as_ref().file_name().unwrap().to_string_lossy().to_string();
let mime = guess_mime_type(path);
let mut content = Vec::with_capacity(file_len as usize);
file.read_to_end(&mut content)?;

Ok(Image {
name,
mime,
content
})
}

///Retrieves configuration of Fie.
pub fn get_config() -> PathBuf {
let mut result = env::current_exe().unwrap();

result.set_file_name(config::NAME);

result
}

0 comments on commit 598ac10

Please sign in to comment.