17 releases (4 major breaking)
2023.10.27 |
|
---|---|
4.0.0 | Feb 29, 2024 |
3.0.0 | Feb 28, 2024 |
2.0.0 | Dec 7, 2023 |
0.9.1 | Nov 23, 2023 |
#1129 in Network programming
142 downloads per month
Used in dovetail
43KB
809 lines
🦊 Fluffer
Fluffer is an experimental crate that aims to make writing Gemini apps fun and easy.
Other helpful gemini projects:
- trotter: A crate for gemini clients.
- schuifkoppel: A reverse-proxy for gemini.
🗼 Design
Similar to Axum, Fluffer routes are generic functions that
can return anything that implements the GemBytes
trait.
There are some helpful implementations out of the box, so
please consult GemBytes
and Fluff
while you
experiment.
Also, this crate has a lot of examples for you to check out. Including a dice roller app.
Here is a basic example of a Fluffer app.
use fluffer::{App, Fluff};
#[tokio::main]
async fn main() {
App::default()
.route("/", |_| async {
"# Welcome\n=> /u32 Should show a number\n=> /pic 🦊 Here's a cool picture!"
})
.route("/u32", |_| async { 777 })
.route("/pic", |_| async { Fluff::File("picture.png".to_string()) })
.run()
.await;
}
💎 GemBytes
The GemBytes
trait returns a Gemini byte response, which
is formatted like this:
<STATUS><SPACE><META>\r\n<CONTENT>
Note: you must include the <SPACE>
character, even if
<META>
is blank.
To implement GemBytes
on a type is to decide which
Gemini response is appropriate for it.
For example: it is sensible to represent some mime-ambiguous data as a successful Gemtext response so it can be read in a client.
use fluffer::{GemBytes, async_trait};
struct Profile {
name: String,
bio: String,
}
#[async_trait]
impl GemBytes for Profile {
async fn gem_bytes(&self) -> Vec<u8> {
format!("20 text/gemini\r\n# {},\n{}", self.name, self.bio).into_bytes()
}
}
📜 Client
Gemini uses client certificates to facilitate identities in geminispace.
Client
provides methods that correspond to common
identity practices in Gemini.
Function | Descripion |
---|---|
Client::certificate |
Get the client cert in the PEM format. |
Client::fingerprint |
Get the client cert's fingerprint (SHA-256). |
Client::name |
Get the client cert's subject_name field. Useful for providing temporary usernames, or just saying hello. |
Client::verify |
Verify that this client's cert is one you're expecting. |
🥴 Input, queries, and parameters
When a gemini client is prompted for input, that input consumes the entire query line.
As such, you should not attach query information to a route that also prompts the user.
Keep this in mind when considering the following options.
Input
To get a user's input to a route, call Client::input
.
This returns the whole query line percent-decoded.
App::default()
.route("/" |c| async {
c.input().unwrap_or("no input 😥".to_string())
})
.run()
.await
.unwrap()
Queries
For routes where you aren't also accounting for a user's input, queries are suitable for tracking UI state across requests.
For example, you can add warning or error messages to a
gemtext document by redirecting to a path with special query
names. (E.g. /home?err=bad%20thingg%20happened
),
The Fluff variant Fluff::RedirectQueries
helps you to do
this by redirecting to a route with a vector of key-value
queries.
You can persist multiple queries by including them in a gemtext link to other pages (or the same page).
Use the method Client::query
to look for query values
which correspond to a key.
Parameters
Parameters are derived from patterns you define on a route's path.
To use a parameter, define it in your route's path string,
and call Client::parameter
in the route method.
App::default()
.route("/page=:number" |c| async {
format!("{}", c.parameter("number").unwrap_or("0"))
})
.run()
.await
.unwrap()
Fluffer uses matchit
. If you're unfamiliar, here's a
couple of examples:
"/owo/:a/:b"
defines parametersa
andb
, e.g:/owo/thisisa/thisisb
"/page=:n/filter=:f
defines the parametern
, andf
following a prefix, e.g:/page=20/filter=date
.
Things to keep in mind:
- Some gemini clients cache pages based on their route. So, you may not want to use parameters for routes that update frequently.
- Every parameter must be included in your url for the route to be found.
- Be careful where you define your parameters. It's possible to consume requests intended for a different route.
- It's more flexible to represent complex expressions as a single parameter, which you parse manually inside the route function.
🌕 Titan
Titan is a sister-protocol to gemini. It allows clients to send data to the server (à la http post). (read more)
This allows titan clients to upload images, videos, and large amounts of text.
To use titan, you must selectively decide which routes will
accept titan data (and how much). You do this by calling
App::titan
instead of App::route
.
Once you've done that, the titan
property on Client
will yield a titan resource if the request was
made with titan.
Here's an example of all that in action.
use fluffer::{App, Client};
async fn index(c: Client) -> String {
if let Some(titan) = c.titan {
return format!(
"Size: {}\nMime: {}\nContent: {}\nToken: {}",
titan.size,
titan.mime,
std::str::from_utf8(&titan.content).unwrap_or("[not utf8]"),
titan.token.unwrap_or(String::from("[no token]")),
);
}
format!(
"Hello, I'm expecting a text/plain gemini request.\n=> titan://{} Click me",
c.url.domain().unwrap_or("")
)
}
#[tokio::main]
async fn main() {
App::default()
.titan("/", index, 20_000_000) // < limits content size to 20mb
.run()
.await
.unwrap()
}
🏃 App State
Currently, Fluffer allows you to add one piece of state that
gets attached as a generic to Client
.
This means you'll need to reflect the app's state in every
reference of Client
, so I recommend using a type alias.
use fluffer::App;
use std::sync::{Arc, Mutex};
// Type alias for Client<State> **highly recommended**
type Client = fluffer::Client<Arc<Mutex<State>>>;
#[derive(Default)]
struct State {
visitors: u32,
}
async fn index(c: Client) -> String {
let mut state = c.state.lock().unwrap();
state.visitors += 1;
format!("Visitors: {}", state.visitors)
}
#[tokio::main]
async fn main() {
let state = Arc::new(Mutex::new(State::default()));
App::default()
.state(state) // <- Must be called first.
.route("/", index)
.run()
.await
.unwrap()
}
✨ Features
Name | Description | Default |
---|---|---|
interactive |
Enables interactive prompt for generating key/cert at runtime. | Yes |
anyhow |
Enables the GemBytes impl for anyhow (not recommended outside of debugging) |
No |
reqwest |
Enables the GemBytes impl for reqwest::Result and reqwest::Response |
No |
📚 Helpful Resources
📋 Todo
- Async for route functions
- Switch to openssl
- Add peer certificate to client
- Spawn threads
- App data
- Titan support
- Add more options to certificate generation
- Tests..?
Dependencies
~9–26MB
~403K SLoC