~boringcactus/gemifedi

360f9f18c0b0b13a20fb29b73161993f85e26490 — Melody Horn 1 year, 1 month ago 811641e v0.2.0
add posting
6 files changed, 94 insertions(+), 11 deletions(-)

M Cargo.lock
M Cargo.toml
M README.md
M src/gemini_util.rs
M src/main.rs
M src/mastodon_util.rs
M Cargo.lock => Cargo.lock +2 -1
@@ 857,7 857,7 @@ dependencies = [

[[package]]
name = "gemifedi"
version = "0.1.0"
version = "0.2.0"
dependencies = [
 "async-std",
 "async-trait",


@@ 868,6 868,7 @@ dependencies = [
 "markup5ever",
 "markup5ever_rcdom",
 "native-tls",
 "percent-encoding 2.1.0",
 "pretty_env_logger",
 "rustls",
 "serde",

M Cargo.toml => Cargo.toml +2 -1
@@ 1,6 1,6 @@
[package]
name = "gemifedi"
version = "0.1.0"
version = "0.2.0"
authors = ["Melody Horn <melody@boringcactus.com>"]
description = "a gemini frontend to the fediverse"
edition = "2018"


@@ 16,6 16,7 @@ maj = { version = "0.6.0", git = "https://tulpa.dev/boringcactus/maj.git", branc
markup5ever = "0.10.0"
markup5ever_rcdom = "0.1.0"
native-tls = { version = "0.2.4", features = ["vendored"] }
percent-encoding = "2.1.0"
pretty_env_logger = "0.4.0"
rustls = "0.18.1"
serde = { version = "1.0.116", features = ["derive"] }

M README.md => README.md +1 -1
@@ 10,7 10,7 @@ currently mostly awful.

## usage

1. get a `gemifedi` binary.
1. get a `gemifedi` binary through either method:

   a. `cargo install --git https://git.sr.ht/~boringcactus/gemifedi`.


M src/gemini_util.rs => src/gemini_util.rs +6 -1
@@ 38,6 38,11 @@ pub fn apply_global_template(nodes: Vec<gemtext::Node>) -> Vec<gemtext::Node> {
    let mut result = vec![
    ];
    result.extend(nodes);
    result.push(gemtext::Node::Link { to: "/about".to_string(), name: Some(format!("powered by gemifedi"))});
    let footer = gemtext::Builder::new()
        .link("/", Some("timeline".to_string()))
        .link("/logout", Some("logout".to_string()))
        .link("/about", Some(format!("about gemifedi v{}", env!("CARGO_PKG_VERSION"))))
        .build();
    result.extend(footer);
    result
}

M src/main.rs => src/main.rs +63 -5
@@ 1,5 1,5 @@
use std::{
    fs::{File, create_dir_all},
    fs::{self, File, create_dir_all},
    io::BufReader,
    path::PathBuf,
    sync::Arc,


@@ 65,7 65,7 @@ fn render_post(post: elefren::entities::status::Status) -> Vec<gemtext::Node> {
    );

    let mut result = vec![
        gemtext::Node::Heading { level: 2, body: post_info },
        gemtext::Node::Heading { level: 3, body: post_info },
    ];
    result.extend(strip_html(&post.content));
    result.extend(vec![


@@ 105,12 105,56 @@ impl Handler {

        let home = mastodon.get_home_timeline().unwrap();

        let before_timeline = gemtext::Builder::new()
            .link("/post", Some("write a new post".to_string()))
            .text("")
            .heading(1, "your timeline")
            .build();

        let posts = home.items_iter()
            .take(10)
            .flat_map(render_post)
            .flat_map(render_post);

        let body = before_timeline.into_iter()
            .chain(posts)
            .collect::<Vec<_>>();

        Ok(GeminiResponse::render(apply_global_template(posts)))
        Ok(GeminiResponse::render(apply_global_template(body)))
    }

    fn post(&self, req: GeminiRequest) -> Result<GeminiResponse, Error> {
        match req.url.query() {
            Some(post) => {
                let path = mastodon_data_path(&req);
                let path = match path {
                    Some(path) => path,
                    None => return Ok(GeminiResponse::need_cert("Need a cert to be able to log in!"))
                };
                let mastodon = match mastodon_toml::from_file(path) {
                    Ok(data) => Mastodon::from(data),
                    Err(_) => return Ok(temp_redirect("/auth"))
                };
                let post = percent_encoding::percent_decode_str(post).decode_utf8_lossy();
                let new_status = StatusBuilder::new()
                    .status(post)
                    .build()
                    .unwrap();
                let status = mastodon.new_status(new_status).unwrap();
                Ok(temp_redirect(format!("/status/{}", status.id)))
            }
            None => {
                let path = mastodon_data_path(&req);
                let path = match path {
                    Some(path) => path,
                    None => return Ok(GeminiResponse::need_cert("Need a cert to be able to log in!"))
                };
                let _mastodon = match mastodon_toml::from_file(path) {
                    Ok(data) => Mastodon::from(data),
                    Err(_) => return Ok(temp_redirect("/auth"))
                };
                Ok(GeminiResponse::input(format!("Post some text")))
            }
        }
    }

    fn auth_begin(&self, req: GeminiRequest) -> Result<GeminiResponse, Error> {


@@ 189,11 233,23 @@ impl Handler {
gemifedi is a Mastodon / Pleroma client for the Gemini protocol. it uses the OAuth API to access your timeline, and stores data keyed by your chosen client certificate. i recommend hosting it yourself instead of trusting someone else to host it for you.

=> https://git.sr.ht/~boringcactus/gemifedi source code and hosting instructions
=> /auth                                    log in on this install of gemifedi
=> /                                        log in on this install of gemifedi
"#);

        Ok(GeminiResponse::render(response))
    }

    fn logout(&self, req: GeminiRequest) -> Result<GeminiResponse, Error> {
        let path = mastodon_data_path(&req);
        let path = match path {
            Some(path) => path,
            None => return Ok(GeminiResponse::need_cert("Need a cert to be able to log in!"))
        };
        if path.exists() {
            fs::remove_file(path).unwrap();
        }
        Ok(GeminiResponse::gemini(b"Logged out successfully".to_vec()))
    }
}

#[async_trait::async_trait]


@@ 202,9 258,11 @@ impl GeminiHandler for Handler {
        let path = req.url.path().to_string();
        route!(path, {
            (/)                         => self.home(req);
            (/"post")                   => self.post(req);
            (/"auth")                   => self.auth_begin(req);
            (/"auth"/[instance])        => self.auth_continue(req, instance);
            (/"auth"/[instance]/"code") => self.auth_finish(req, instance);
            (/"logout")                 => self.logout(req);
            (/"status"/[id])            => self.status(req, id);
            (/"about")                  => self.about(req);
        });

M src/mastodon_util.rs => src/mastodon_util.rs +20 -2
@@ 6,6 6,7 @@ use std::{
use elefren::{
    prelude::*,
    registration::Registered,
    scopes,
};
use serde::{Serialize, Deserialize};



@@ 14,21 15,38 @@ struct AppInstall {
    parts: (String, String, String, String, Scopes, bool),
}

pub fn scopes_needed() -> Scopes {
    Scopes::read(scopes::Read::Statuses)
        | Scopes::read(scopes::Read::Notifications)
        | Scopes::write(scopes::Write::Statuses)
        | Scopes::write(scopes::Write::Notifications)
        | Scopes::write(scopes::Write::Favourites)
}

pub fn get_app_install(domain: &str) -> Registered<elefren::http_send::HttpSender> {
    let mut data_path = PathBuf::from("data");
    data_path.push("domains");
    create_dir_all(&data_path).unwrap();
    data_path.push(format!("{}.toml", domain));

    let scopes_needed = scopes_needed();

    if data_path.exists() {
        let file_data = fs::read(data_path).unwrap();
        let file_data = fs::read(&data_path).unwrap();
        let install: AppInstall = toml::from_slice(file_data.as_slice()).unwrap();
        let (base, client_id, client_secret, redirect, scopes, force_login) = install.parts;
        dbg!(&scopes, &scopes_needed);
        if scopes != scopes_needed {
            // we don't have the scopes we need, so re-register on this domain
            let _ = fs::remove_file(data_path);
            return get_app_install(domain);
        }
        Registered::from_parts(&base, &client_id, &client_secret, &redirect, scopes, force_login)
    } else {
        let registered = Registration::new(format!("https://{}", domain))
            .client_name("gemifedi")
            .client_name(concat!("gemifedi v", env!("CARGO_PKG_VERSION")))
            .website("https://git.sr.ht/~boringcactus/gemifedi")
            .scopes(scopes_needed)
            .build()
            .unwrap();
        let parts = registered.clone().into_parts();