Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ openssl = { version = "0.10", features = ["vendored"] }
directories = "5.0.1"
log = "0.4.22"
scraper = "0.21.0"
ureq = "2"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is unused, isn't it? If yes we can remove it

112 changes: 110 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use clap::Parser;
use data_url::DataUrl;
use url::Url;
use reqwest::blocking::Client;
use pbcli::api::API;
use pbcli::error::{PasteError, PbResult};
use pbcli::opts::Opts;
use pbcli::privatebin::{DecryptedComment, DecryptedCommentsMap, DecryptedPaste};
use pbcli::util::check_filesize;
use serde_json::Value;
use std::time::Duration;
use std::ffi::OsString;
use std::io::IsTerminal;
use std::io::{Read, Write};
Expand Down Expand Up @@ -99,6 +102,86 @@ fn handle_get(opts: &Opts) -> PbResult<()> {
Ok(())
}

fn shorten_via_privatebin(opts: &Opts, long_url: &str) -> PbResult<String> {
fn try_method(opts: &Opts, long_url: &str, method: &str) -> PbResult<String> {
let encoded = url::form_urlencoded::byte_serialize(long_url.as_bytes()).collect::<String>();

// Always shorten on the same host as --host
let mut endpoint = opts.get_url().clone();
endpoint.set_fragment(None);
endpoint.set_path("/");
endpoint.set_query(Some(&format!("{method}&link={encoded}")));

let client = Client::builder().timeout(Duration::from_secs(5)).build()?;
let resp_text = client
.get(endpoint.clone())
.send()?
.error_for_status()?
.text()?;

let text = resp_text.trim();

log::debug!("shortener ({}) GET: {}", method, endpoint);
log::debug!("shortener ({}) raw response: {}", method, text);

// JSON first (some proxies return JSON)
if let Ok(v) = serde_json::from_str::<Value>(text) {
if let Some(s) = v.get("shorturl").and_then(|x| x.as_str()) {
return Ok(s.to_string());
}
if let Some(s) = v.get("url").and_then(|x| x.as_str()) {
return Ok(s.to_string());
}
}

// HTML: accept only <a id="pasteurl" href="...">
if let Some(id_pos) = text.find("id=\"pasteurl\"") {
let after_id = &text[id_pos..];

if let Some(href_pos) = after_id.find("href=\"") {
let after_href = &after_id[href_pos + "href=\"".len()..];
if let Some(end) = after_href.find('"') {
let url = &after_href[..end];
if url.starts_with("https://") || url.starts_with("http://") {
return Ok(url.to_string());
}
}
}

if let Some(href_pos) = after_id.find("href='") {
let after_href = &after_id[href_pos + "href='".len()..];
if let Some(end) = after_href.find('\'') {
let url = &after_href[..end];
if url.starts_with("https://") || url.starts_with("http://") {
return Ok(url.to_string());
}
}
}
Comment on lines +141 to +159
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as below should be possible here to reduce code duplication, since I believe the only difference is ' or ":

Turn those into an array and use find_map (like below) to get the first non-None result

}

Err(PasteError::InvalidData)
}

// 1) try YOURLS proxy first
match try_method(opts, long_url, "shortenviayourls") {
Ok(u) => Ok(u),
Err(e1) => {
log::debug!("YOURLS shorten failed ({e1:?}), trying shlink…");

// 2) fall back to shlink proxy
match try_method(opts, long_url, "shortenviashlink") {
Ok(u) => Ok(u),
Err(e2) => {
// preserve useful debug, but return a single error
log::debug!("Shlink shorten also failed ({e2:?})");
Err(PasteError::InvalidData)
}
}
}
}
Comment on lines +166 to +181
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a nested retry, I would propose a different construct:

turn the methods into an array and loop over it, aborting on the first non-None result, this can be done with
https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.find_map

}


fn handle_post(opts: &Opts) -> PbResult<()> {
let url = opts.get_url();
let stdin = get_stdin()?;
Expand Down Expand Up @@ -135,14 +218,39 @@ fn handle_post(opts: &Opts) -> PbResult<()> {
}

let res = api.post_paste(&paste, password, opts)?;
let long_url = res.to_paste_url().to_string();

let should_shorten = opts.shorten && !opts.no_shorten;

let short_url = if should_shorten {
match shorten_via_privatebin(opts, &long_url) {
Ok(s) => Some(s),
Err(e) => {
log::debug!("shorten failed, falling back to long URL: {e:?}");

// User-facing notice (stderr so stdout remains just the URL)
eprintln!(
"--shorten specified but no URL shortener was found configured on host, falling back to host-provided URL."
);

None
}
}
} else {
None
};

if opts.json {
let mut output: Value = serde_json::to_value(res.clone())?;
output["pasteurl"] = Value::String(res.to_paste_url().to_string());
output["pasteurl"] = Value::String(long_url.clone());
output["deleteurl"] = Value::String(res.to_delete_url().to_string());
if let Some(s) = &short_url {
output["shorturl"] = Value::String(s.clone());
}
std::io::stdout().write_all(serde_json::to_string_pretty(&output)?.as_bytes())?;
} else {
std::io::stdout().write_all(res.to_paste_url().as_str().as_bytes())?;
let to_print = short_url.as_deref().unwrap_or(&long_url);
std::io::stdout().write_all(to_print.as_bytes())?;
writeln!(std::io::stdout())?;
}

Expand Down
7 changes: 7 additions & 0 deletions src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ pub struct Opts {
#[clap(help("disable burn if set"))]
pub no_discussion: bool,

#[cfg_attr(feature = "uniffi", uniffi(default = false))]
#[clap(long, help("Shorten the resulting paste URL via PrivateBin's YOURLS proxy"))]
pub shorten: bool,

#[clap(long, overrides_with = "shorten")]
pub no_shorten: bool,

#[cfg_attr(feature = "uniffi", uniffi(default = false))]
#[clap(long, requires("url"))]
#[clap(help("make new comment on existing paste"))]
Expand Down