Browse Source

New Plume API: no more canapi + Better error handling

Fix #4
Fix #5
master
Baptiste Gelez 2 years ago
parent
commit
9a8502cb7d
5 changed files with 148 additions and 129 deletions
  1. +4
    -4
      .env
  2. +4
    -15
      Cargo.lock
  3. +3
    -3
      Cargo.toml
  4. +1
    -1
      rust-toolchain
  5. +136
    -106
      src/main.rs

+ 4
- 4
.env View File

@@ -1,5 +1,5 @@
PLUME_API_URL=localhost:2020
PLUME_API_URL=pr-540.joinplu.me

PLUME_CLIENT_ID=b62af083cd34da7ad36fbf1f2133b5c74ad16e05da9fcabdcb8b6672e63
PLUME_CLIENT_SECRET=b22c28a4e6cf9a9eb9c4749aefaf4b26afa38ac7c6f6dd42aa324e4cce224e
PLUME_API_TOKEN=c7e38c11bc1543637f9e85a123513b4c22e3029c74cb6b6412aa3979d22d11a
PLUME_CLIENT_ID="e1a2e321542191e6e9d74a60e6f4803d93d956bd7efd3d19777056c6bcf9e299"
PLUME_CLIENT_SECRET="43db6ef980d85fa332ca49c6fc5cdc8e5912d3d7e765a5a9fa53783dac738"
PLUME_API_TOKEN=c59536eab888ed4a04e7074a3df7d29a9b68ae2547a9694c7924068e4bc89

+ 4
- 15
Cargo.lock View File

@@ -15,10 +15,9 @@ dependencies = [
name = "amsterdam"
version = "0.1.0"
dependencies = [
"canapi 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
"dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
"plume-api 0.2.0 (git+https://github.com/Plume-org/Plume?rev=88a6a81ad8ce8cdb18b9ae3467961cf9490e4411)",
"plume-api 0.3.0 (git+https://github.com/Plume-org/Plume?rev=aee604751193aaebb0a858dc81dc15a9ab67659e)",
"reqwest 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -104,14 +103,6 @@ dependencies = [
"iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "canapi"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "cc"
version = "1.0.25"
@@ -660,10 +651,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "plume-api"
version = "0.2.0"
source = "git+https://github.com/Plume-org/Plume?rev=88a6a81ad8ce8cdb18b9ae3467961cf9490e4411#88a6a81ad8ce8cdb18b9ae3467961cf9490e4411"
version = "0.3.0"
source = "git+https://github.com/Plume-org/Plume?rev=aee604751193aaebb0a858dc81dc15a9ab67659e#aee604751193aaebb0a858dc81dc15a9ab67659e"
dependencies = [
"canapi 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1309,7 +1299,6 @@ dependencies = [
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
"checksum byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "90492c5858dd7d2e78691cfb89f90d273a2800fc11d98f60786e5d87e2f83781"
"checksum bytes 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0ce55bd354b095246fc34caf4e9e242f5297a7fd938b090cadfea6eee614aa62"
"checksum canapi 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aab4d6d1edcef8bf19b851b7730d3d1a90373c06321a49a984baebe0989c962c"
"checksum cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16"
"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e"
@@ -1372,7 +1361,7 @@ dependencies = [
"checksum phf_generator 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)" = "03dc191feb9b08b0dc1330d6549b795b9d81aec19efe6b4a45aec8d4caee0c4b"
"checksum phf_shared 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)" = "b539898d22d4273ded07f64a05737649dc69095d92cb87c7097ec68e3f150b93"
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
"checksum plume-api 0.2.0 (git+https://github.com/Plume-org/Plume?rev=88a6a81ad8ce8cdb18b9ae3467961cf9490e4411)" = "<none>"
"checksum plume-api 0.3.0 (git+https://github.com/Plume-org/Plume?rev=aee604751193aaebb0a858dc81dc15a9ab67659e)" = "<none>"
"checksum proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)" = "3d7b7eaaa90b4a90a932a9ea6666c95a389e424eff347f0f793979289429feee"
"checksum quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "dd636425967c33af890042c483632d33fa7a18f19ad1d7ea72e8998c6ef8dea5"
"checksum rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e464cd887e869cddcae8792a4ee31d23c7edd516700695608f5b98c67ee0131c"


+ 3
- 3
Cargo.toml View File

@@ -1,14 +1,14 @@
[package]
name = "amsterdam"
version = "0.1.0"
authors = ["Bat' <baptiste@gelez.xyz>"]
authors = ["Ana Gelez <ana@gelez.xyz>"]
license = "GPLv3.0"
edition = "2018"

[dependencies]
canapi = "0.2.0"
clap = "2.0"
dotenv = "0.13"
plume-api = { git = "https://github.com/Plume-org/Plume", rev = "88a6a81ad8ce8cdb18b9ae3467961cf9490e4411" }
plume-api = { git = "https://github.com/Plume-org/Plume", rev = "aee604751193aaebb0a858dc81dc15a9ab67659e" }
reqwest = "0.9.3"
rpassword = "2.0.0"
serde_json = "1.0"

+ 1
- 1
rust-toolchain View File

@@ -1 +1 @@
nightly-2018-07-17
nightly-2018-12-06

+ 136
- 106
src/main.rs View File

@@ -1,76 +1,94 @@
extern crate canapi;
extern crate clap;
extern crate dotenv;
extern crate plume_api;
extern crate serde_json;
extern crate reqwest;
extern crate rpassword;

use canapi::*;
use clap::{App, Arg, ArgMatches, SubCommand};
use dotenv::dotenv;
use plume_api::{apps::AppEndpoint, posts::PostEndpoint};
use reqwest::{ClientBuilder, Method};
use std::{io::{self, prelude::*, BufRead, BufReader}, env, fs::{metadata, File, OpenOptions}, str::FromStr, fmt::Display};
use plume_api::{apps::NewAppData, posts::NewPostData};
use reqwest::Client;
use std::{
env,
fmt::Display,
fs::{metadata, File, OpenOptions},
io::{self, prelude::*, BufRead, BufReader},
};

fn main() {
let client = Client::new();

if let Err(e) = run(&client) {
println!("Error: {}", e);
}
}

fn run(client: &Client) -> Result<(), String> {
dotenv().ok();
let mut app = App::new("Amsterdam")
.bin_name("amsterdam")
.version(env!("CARGO_PKG_VERSION"))
.about("Import articles to Plume.")
.subcommand(SubCommand::with_name("md")
.arg(Arg::with_name("FILES")
.takes_value(true)
.multiple(true)
.help("The Markdown files to import"))
.about("Import Markown files")
.subcommand(
SubCommand::with_name("md")
.arg(
Arg::with_name("FILES")
.takes_value(true)
.multiple(true)
.help("The Markdown files to import"),
)
.about("Import Markown files"),
);

if env::var("PLUME_API_TOKEN").is_err() {
get_token();
get_token(&client)?;
}

match app.clone().get_matches().subcommand() {
("md", Some(args)) => md(args),
_ => app.print_help().unwrap(),
("md", Some(args)) => md(args, client),
_ => app.print_help().map_err(|_| "Try 'amsterdam md'".to_string()),
}
}

fn write_to_env<A: Display, B: Display>(var: A, val: B) {
fn write_to_env<A: Display, B: Display>(var: A, val: B) -> Result<(), String> {
let mut file = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(".env")
.unwrap();
.map_err(|_| "Couldn't open .env")?;

writeln!(file, "{}={}", var, val).expect("Couldn't write to file");
writeln!(file, "{}={}", var, val).map_err(|_| "Couldn't write to .env")?;
dotenv().ok();
Ok(())
}

fn make_client() -> (String, String) {
fn make_client(req: &Client) -> Result<(String, String), String> {
println!("Please enter your instance domain (without https:// or http://)");
let mut url = String::new();
io::stdin().read_line(&mut url).ok();
write_to_env("PLUME_API_URL", url);

let client = AppEndpoint::default().create::<ReqwestFetch>(AppEndpoint {
name: String::from("Amsterdam"),
..AppEndpoint::default()
});
if let Ok(client) = client {
write_to_env("PLUME_CLIENT_ID", client.client_id.clone().unwrap());
write_to_env("PLUME_CLIENT_SECRET", client.client_secret.clone().unwrap());
(client.client_id.unwrap(), client.client_secret.unwrap())
} else {
panic!("client is err");
}
write_to_env("PLUME_API_URL", url)?;

let client: serde_json::Value = req
.post(&format!(
"https://{}/api/v1/apps",
env::var("PLUME_API_URL").unwrap()
))
.json(&NewAppData {
name: "Amsterdam".into(),
website: Some("https://github.com/Plume-org/amsterdam".into()),
redirect_uri: None,
})
.send()
.and_then(|mut r| r.json())
.map_err(|_| "Couldn't register the Amsterdam app".to_string())?;

write_to_env("PLUME_CLIENT_ID", client["client_id"].to_string())?;
write_to_env("PLUME_CLIENT_SECRET", client["client_secret"].to_string())?;
Ok((
env::var("PLUME_CLIENT_ID").unwrap(),
env::var("PLUME_CLIENT_SECRET").unwrap(),
))
}

fn get_token() {
fn get_token(client: &Client) -> Result<(), String> {
let (c_id, c_secret) = env::var("PLUME_CLIENT_ID")
.and_then(|id| env::var("PLUME_CLIENT_SECRET").map(|sec| (id, sec)))
.unwrap_or_else(|_| make_client());
.or_else(|_| make_client(&client))?;
println!("What is your username?");
let mut name = String::new();
io::stdin().read_line(&mut name).ok();
@@ -87,90 +105,102 @@ fn get_token() {
c_secret,
);

let json: serde_json::Value = ClientBuilder::new().danger_accept_invalid_certs(true).build().unwrap()
.get(url.as_str()).send().and_then(|mut r| r.text())
.map(|t| serde_json::from_str(t.as_str()).unwrap())
.unwrap();
let json: serde_json::Value = client
.get(&url)
.send()
.and_then(|mut r| r.json())
.map_err(|_| "Couldn't obtain an API token")?;
write_to_env("PLUME_API_TOKEN", json["token"].as_str().unwrap())
}

enum ParseState {
Ready,
FrontMatter(PostEndpoint),
Body(PostEndpoint),
FrontMatter(NewPostData),
Body(NewPostData),
}

fn md<'a>(args: &'a ArgMatches) {
for path in args.values_of("FILES").unwrap() {
if metadata(path).expect("File not found").is_file() {
println!("Importing {}", path);

let f = File::open(path).expect("File not found");
fn md<'a>(args: &'a ArgMatches, client: &Client) -> Result<(), String> {
for path in args
.values_of("FILES")
.ok_or("Please provide files to upload".to_string())?
{
if metadata(path)
.or(Err(format!("Couldn't read {}", path)))?
.is_file()
{
println!("Importing {}…", path);

let f = File::open(path).or(Err(format!("Couldn't read {}", path)))?;
let file = BufReader::new(f);
let parse_result = file.lines().fold(ParseState::Ready, |state, line| {
let line = line.expect("Couldn't read line");
match state {
ParseState::Ready => if line.chars().all(|c| c == '-') {
ParseState::FrontMatter(PostEndpoint::default())
} else {
panic!("Your articles should use FrontMatter metadata")
},
ParseState::FrontMatter(mut post) => {
let mut parsed = line.splitn(2, ':');
let mut field = parsed.next().expect("No metadata field");
if let Some(value) = parsed.next() {
let value = value.trim();
match field {
"title" => post.title = Some(value.to_string()),
"subtitle" => post.subtitle = Some(value.to_string()),
"tags" => post.tags = Some(value.split(",").map(str::trim).map(String::from).collect()),
"date" => post.creation_date = Some(value.to_string()),
x => println!("The {} field will be ignored.", x),
};
ParseState::FrontMatter(post)
} else {
ParseState::Body(post)
let line = line.map_err(|_| println!("Couldn't read {}", path)).ok();
if let Some(line) = line {
match state {
ParseState::Ready => {
if line.chars().all(|c| c == '-') {
let mut post = NewPostData::default();
post.published = Some(true);
ParseState::FrontMatter(post)
} else {
ParseState::Ready
}
}
ParseState::FrontMatter(mut post) => {
let mut parsed = line.splitn(2, ':');
let field = parsed.next().expect("THIS IS NOT POSSIBLE");
if let Some(value) = parsed.next() {
let value = value.trim();
match field {
"title" => post.title = value.to_string(),
"subtitle" => post.subtitle = Some(value.to_string()),
"tags" => {
post.tags = Some(
value
.split(",")
.map(str::trim)
.map(String::from)
.collect(),
)
}
"date" => post.creation_date = Some(value.to_string()),
"license" => post.license = Some(value.to_string()),
x => println!("WARNING: The {} field will be ignored.", x),
};
ParseState::FrontMatter(post)
} else {
ParseState::Body(post)
}
}
},
ParseState::Body(mut post) => {
if let Some(source) = post.source {
post.source = Some(source + "\n" + line.as_ref());
} else {
post.source = Some(line);
ParseState::Body(mut post) => {
post.source = post.source + "\n" + line.as_ref();
ParseState::Body(post)
}
ParseState::Body(post)
}
} else {
state
}
});

if let ParseState::Body(article) = parse_result {
println!("Publishing {}...", article.title.clone().unwrap());
PostEndpoint::default().create::<ReqwestFetch>(article).ok();
println!("Publishing {}…", article.title);

client
.post(&format!(
"https://{}/api/v1/posts/",
env::var("PLUME_API_URL").unwrap()
))
.json(&article)
.bearer_auth(env::var("PLUME_API_TOKEN").unwrap_or(String::new()))
.send()
.map_err(|_| "Error during publication")?
.json::<serde_json::Value>()
.map_err(|_| "Couldn't read response as JSON")?
["error"].as_str().map(|e| println!("ERROR: The API returned: {}\nMake sure you have created a blog on Plume.", e));
} else {
panic!("Error while parsing article metadata");
println!("ERROR: Couldn't parse article metadata for {}", path);
}
}
}
}

struct ReqwestFetch;

impl Fetch for ReqwestFetch {
fn fetch<T: Endpoint>(method: &'static str, url: String, data: Option<T>) -> Result<T, Error> {
ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap()
.request(
Method::from_str(method).expect("Canapi: invalid method"),
format!("https://{}{}", env::var("PLUME_API_URL").unwrap(), url).as_str()
)
.body(serde_json::to_string(&data).expect("Error while serializing post"))
.header("Content-Type", "application/json")
.bearer_auth(env::var("PLUME_API_TOKEN").unwrap_or(String::new()))
.send()
.and_then(|mut r| r.text())
.map_err(|e| { println!("err: {}", e.to_string()); Error::Fetch(e.to_string()) })
.and_then(|t| { println!("got: {}", t); serde_json::from_str(t.as_ref()).map_err(|e| Error::SerDe(e.to_string())) })
}
println!(""); // Just print a blank line to separate each import
Ok(())
}

Loading…
Cancel
Save