Compare commits

..

1 Commits

Author SHA1 Message Date
21e1bb2173 See if can build for linux
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-22 15:42:28 +00:00
27 changed files with 1594 additions and 5893 deletions

5
.gitignore vendored
View File

@ -1,11 +1,6 @@
target/
target-linux/
cli/capsule/
api/capsules/
*.sw*
db.db3
cert.pem
key.rsa
content/
.DS_Store
build/

View File

@ -1,11 +1,19 @@
pipeline:
buildCli:
group: build
image: rust
commands:
- cd cli
- rustup target add x86_64-apple-darwin
- cargo build --release --target x86_64-unknown-linux-gnu
buildApi:
group: build
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
when:
path: "api/**/*"
path: "api/*"
settings:
repo: wilw/capsuletown-api
dockerfile: api/Dockerfile
@ -16,46 +24,22 @@ pipeline:
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
when:
path: "server/**/*"
path: "server/*"
settings:
repo: wilw/capsuletown-server
dockerfile: server/Dockerfile
context: server
buildCli:
group: build
image: rust
when:
path: "cli/**/*"
commands:
- cd cli
- cargo build --release
- target/release/captown help
deployRootCapsule:
group: deploy
image: python:3.10
group: build
when:
path: "home/**/*"
path: "home/*"
secrets: [ CAPSULE_TOWN_KEY ]
commands:
- cd home
- CAPSULE=$(tar -czf /tmp/c.tar.gz -C capsule . && cat /tmp/c.tar.gz | base64)
- 'echo "{\"capsuleArchive\": \"$CAPSULE\"}" > /tmp/capsule_file'
- 'curl -X PUT -H "Content-Type: application/json" -H "api-key: $CAPSULE_TOWN_KEY" -d @/tmp/capsule_file https://api.capsule.town/capsule'
deployCli:
group: deploy
image: alpine
when:
path: "cli/**/*"
secrets: [ LINODE_ACCESS_KEY, LINODE_SECRET_ACCESS_KEY, BUNNY_KEY ]
commands:
- cd cli
- apk update
- apk add s3cmd curl
- s3cmd --configure --access_key=$LINODE_ACCESS_KEY --secret_key=$LINODE_SECRET_ACCESS_KEY --host=https://eu-central-1.linodeobjects.com --host-bucket="%(bucket)s.eu-central-1.linodeobjects.com" --dump-config > /root/.s3cfg
- s3cmd -c /root/.s3cfg put target/release/captown s3://download.capsule.town/captown-linux-0.2 --acl-public
- 'curl -X POST -H "AccessKey: $BUNNY_KEY" https://api.bunny.net/pullzone/783552/purgeCache'
branches: main
#branches: main

View File

@ -1,37 +0,0 @@
version: '3'
tasks:
default:
desc: Run API
deps:
- run-api
run-api:
desc: Run API server
dir: 'api'
dotenv: ['envfile']
cmds:
- cargo run
deploy-binary:
desc: Deploy ARM macOS and AMD64 Linux binary
dir: 'cli'
cmds:
- cargo build -r
- docker run --platform linux/amd64 --rm --user "$(id -u)":"$(id -g)" -v "$PWD":/usr/src/app -v "$PWD/target-linux":/usr/src/app/target -w /usr/src/app rust cargo build --release
- aws --profile personal s3 cp target/release/captown s3://download.capsule.town/captown-mac-0.3 --acl public-read
- aws --profile personal s3 cp target-linux/release/captown s3://download.capsule.town/captown-linux-0.3 --acl public-read
- 'curl -X POST -H "AccessKey: $BUNNY_PERSONAL" https://api.bunny.net/pullzone/783552/purgeCache'
deploy-api:
desc: Deploy API
dir: 'api'
cmds:
- docker build -t wilw/capsuletown-api --platform linux/amd64 .
- docker push wilw/capsuletown-api
deploy-root-capsule:
desc: Deploy the root capsule to capsule.town
dir: 'home'
cmds:
- captown publish -c root

2746
api/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,18 @@
[package]
name = "gem-api"
version = "0.1.1"
version = "0.1.0"
authors = ["Will Webberley <me@wilw.dev>"]
edition = "2021"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = { version = "0.5.1", features = ["json"] }
jsonwebtoken = "9.3.1"
serde = { version = "1.0.218", features = ["derive"] }
flate2 = "1.1.0"
tar = "0.4.44"
base64 = "0.22.1"
rusqlite = "0.33.0"
uuid = { version = "1.15.1", features = ["v4"] }
ubyte = "0.10.4"
reqwest = { version = "0.12.12", features = [] }
tokio = "1.43.0"
tempfile = "3.17.1"
rocket = "0.4.8"
rocket_contrib = "0.4.6"
jsonwebtoken = "7.2"
serde = { version = "1.0", features = ["derive"] }
flate2 = "1.0.19"
tar = "0.4.30"
base64 = "0.13.0"
rusqlite = "0.24.2"
uuid = { version = "0.8", features = ["v4"] }

View File

@ -1,13 +1,12 @@
FROM rust:latest
from rustlang/rust:nightly
RUN apt-get update
RUN apt-get install sqlite3
RUN mkdir /app
RUN mkdir /app/src
WORKDIR /app
ADD ./src/*.rs ./src/
ADD ./src/main.rs ./src/main.rs
ADD Cargo.toml .
ADD Cargo.lock .

View File

@ -1,21 +0,0 @@
# capsule.town API
This directory contains the source code for the capsule.town web API. It's a simple set of HTTP endpoints for managing Gemini capsules on capsule.town.
Typically it's invoked using the capsule.town CLI, but it doesn't have to be.
## Running locally for development
We recommend using Taskfile to run the API locally (as this sets up the environment, etc. too). Simply run the following in the project root:
```
task
```
## Deploy the API
The capsule.town API currently runs on an AMD64 Linux machine. Docker is used to cross-compile the image:
```
task deploy-api
```

View File

@ -1,9 +0,0 @@
export CAPSULE_DOMAIN="capsule.town"
export JWT_SECRET="secret"
export DB_PATH="db.db3"
export CAPSULES_PATH="capsules"
export FROM_EMAIL="capsule.town <hello@mail.capsule.town>"
export ADMIN_EMAIL="hello@capsule.town"
export MAILGUN_URL=""
export MAILGUN_KEY=""

View File

@ -1,50 +0,0 @@
use std::collections::HashMap;
use reqwest::{Client};
use tokio;
use crate::util;
pub fn send(to: &str, subject: &str, body: &str) -> Result<bool, bool> {
let to_string = to.to_string();
let subject_string = subject.to_string();
let body_string = body.to_string();
tokio::spawn(async move {
if let Err(e) = send_thread(to_string, subject_string, body_string).await {
eprintln!("Failed to send email: {}", e);
}
});
Ok(true)
}
async fn send_thread(to: String, subject: String, body: String) -> Result<(), Box<dyn std::error::Error>> {
let mailgun_url = util::get_env_var("MAILGUN_URL");
let mailgun_key = util::get_env_var("MAILGUN_KEY");
if mailgun_url.is_empty() || mailgun_key.is_empty() {
println!("No Mailgun credentials set. Not sending mail, but printing below:\n{}", &body);
return Ok(());
}
let mut map = HashMap::new();
map.insert("to", to);
map.insert("from", util::get_env_var("FROM_EMAIL"));
map.insert("subject", subject);
map.insert("text", body);
let client = Client::new();
let response = client
.post(mailgun_url)
.basic_auth("api", Some(mailgun_key))
.form(&map)
.send()
.await?;
if response.status().is_success() {
println!("Email sent successfully!");
Ok(())
} else {
let status = response.status();
let error_text = &response.text().await?;
eprintln!("Failed to send email: {} ({})", error_text, status);
Err(format!("HTTP Error: {}", status).into())
}
}

View File

@ -1,47 +1,97 @@
// Copyright (c) 2025, Will Webberley. See LICENSE.
// Copyright (c) 2021, Will Webberley. See LICENSE.
use base64::{engine::general_purpose, Engine as _};
use tempfile::tempdir;
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket_contrib;
use std::{env, str, fs::{self, File}, time::{SystemTime, UNIX_EPOCH}, io::Write};
use rocket::{self, routes, get, post, put, config::{Config, Limits}, response::Responder, request::{self, Request, FromRequest}, Outcome, http::Status};
use rocket_contrib::json::{Json, JsonValue};
use serde::{Serialize, Deserialize};
use jsonwebtoken::{self, encode, decode, Header, Validation, EncodingKey, DecodingKey};
use rusqlite::{params, Connection};
use flate2::read::GzDecoder;
use rocket::{
self, catch, catchers,
config::Config,
data::Limits,
get,
launch, post, put,
request::{Request},
response::Responder,
routes,
serde::json::{json, Json, Value as JsonValue},
};
use std::net::{IpAddr, Ipv4Addr};
use rusqlite::{params};
use serde::{Deserialize, Serialize};
use std::{
fs::{self, File},
io::Write,
str,
};
use tar::Archive;
use ubyte::ToByteUnit;
use base64;
use uuid::Uuid;
mod mail;
mod util;
const DB_FILE: &str = "/usr/local/var/capsules_db/db.db3";
const CAPSULE_DIR: &str = "/usr/local/var/capsules";
const TMP_FILE: &str = "/tmp/capsulearchive.tar.gz"; // TODO: use tempfile https://docs.rs/tempfile/3.2.0/tempfile/index.html
const BLOCKLIST: [&str; 9] = ["gemini", "capsule", "root", "webmaster", "admin", "www", "mail", "email", "api"];
const DEV_JWT_SECRET: &str = "secret";
const DEFAULT_DOMAIN: &str = "capsule.town";
// A list of forbidden capsule names
const BLOCKLIST: [&str; 9] = [
"gemini",
"capsule",
"root",
"webmaster",
"admin",
"www",
"mail",
"email",
"api",
];
type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
// Utility function: returns a u64 containing the current number of seconds since UNIX EPOCH
fn get_current_time() -> i64 {
return match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs() as i64,
Err(_) => 0 as i64,
};
}
// Utility function to retrieve a connection to the database
fn get_db() -> Result<Connection, rusqlite::Error> {
return Ok(Connection::open(DB_FILE)?);
}
// Utility function to return the domain for this API server
fn get_domain() -> String {
return match env::var("CAPSULE_DOMAIN") {
Ok(x) => x,
Err(_) => String::from(DEFAULT_DOMAIN),
};
}
// Utility function to return the currently in-play JWT secret
fn get_jwt_secret() -> String {
return match env::var("JWT_SECRET") {
Ok(x) => x,
Err(_) => String::from(DEV_JWT_SECRET),
};
}
// A struct to contain token data to embed in JWT tokens
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
}
struct AuthenticatedCapsule {
id: String,
name: String,
}
// Generate a JWT token, embedding the capsule ID as the token `sub`. Returns the token as a
// String.
fn generate_access_token(capsule_id: &str) -> Result<String> {
let my_claims = Claims {
sub: capsule_id.to_owned(),
};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(get_jwt_secret().as_ref()))?;
return Ok(token);
}
// Verify the access JWT token, and checks still valid in DB. Returns the authenticated capsule.
fn validate_access_token(token: &str) -> Result<AuthenticatedCapsule> {
// Validate the token integrity itself
let my_validation = Validation {
validate_exp: false,
..Default::default()
};
let claims = decode::<Claims>(&token, &DecodingKey::from_secret(get_jwt_secret().as_ref()), &my_validation)?.claims;
// Verify the token in the DB and retrieve the capsule info
let conn = get_db()?;
let mut stmt = conn.prepare("SELECT capsule.id, name FROM capsule
INNER JOIN token ON capsule.id = token.capsule_id
WHERE capsule.id = ?1 AND token.token = ?2")?;
let mut rows = stmt.query(params![claims.sub, &token])?;
let first_row_result = rows.next()?;
if let Some(row) = first_row_result {
return Ok(AuthenticatedCapsule { id: row.get(0)?, name: row.get(1)? });
} else {
return Err(Box::from(rusqlite::Error::QueryReturnedNoRows));
}
}
#[derive(Responder)]
enum Resp {
@ -55,11 +105,6 @@ enum Resp {
NotFound(JsonValue),
}
#[catch(400)]
fn bad_request(_: &Request) -> Resp {
return Resp::BadRequest(json!({"message": "There was a problem with your request"}));
}
#[get("/")]
fn index() -> Resp {
return Resp::NotFound(json!({"message": "There is no resource available at this path."}));
@ -73,7 +118,7 @@ struct CapsuleCreateRequest {
}
#[post("/capsule", data = "<site>")]
fn create_capsule(site: Json<CapsuleCreateRequest>) -> Resp {
let conn = match util::get_db() {
let conn = match get_db() {
Ok(c) => c,
Err(_) => return Resp::InternalServerError(json!({"message": "Database problem"})),
};
@ -86,20 +131,16 @@ fn create_capsule(site: Json<CapsuleCreateRequest>) -> Resp {
created INTEGER NOT NULL
)",
params![],
) {
return Resp::InternalServerError(json!({"message": "Database problem"}));
};
) { return Resp::InternalServerError(json!({"message": "Database problem"})); };
if let Err(_) = conn.execute(
"CREATE TABLE IF NOT EXISTS token (
id TEXT PRIMARY KEY,
id TEXT PRIMARY KEY,
capsule_id TEXT NOT NULL,
token TEXT NOT NULL,
created INTEGER NOT NULL
)",
params![],
) {
return Resp::InternalServerError(json!({"message": "Database problem"}));
};
) { return Resp::InternalServerError(json!({"message": "Database problem"})); };
if let Err(_) = conn.execute(
"CREATE TABLE IF NOT EXISTS contact_detail (
capsule_id TEXT NOT NULL,
@ -107,20 +148,12 @@ fn create_capsule(site: Json<CapsuleCreateRequest>) -> Resp {
created INTEGER NOT NULL
)",
params![],
) {
return Resp::InternalServerError(json!({"message": "Database problem"}));
};
) { return Resp::InternalServerError(json!({"message": "Database problem"})); };
let capsule_name = String::from(site.capsule_name.to_lowercase().trim());
if BLOCKLIST.iter().any(|element| element == &capsule_name)
|| !capsule_name.chars().all(char::is_alphanumeric)
|| capsule_name.len() < 3
|| capsule_name.len() > 20
{
return Resp::BadRequest(
json!({"message": "Your capsule name is invalid. It can only contain alphanumeric characters and must have a length longer than 3 and not use a protected keyword."}),
);
if BLOCKLIST.iter().any(|element| element == &capsule_name) || !capsule_name.chars().all(char::is_alphanumeric) || capsule_name.len() < 3 || capsule_name.len() > 20 {
return Resp::BadRequest(json!({"message": "Your capsule name is invalid. It can only contain alphanumeric characters and must have a length longer than 3 and not use a protected keyword."}));
}
// Check if capsule with this name exists. If so, return BadRequest.
@ -130,26 +163,20 @@ fn create_capsule(site: Json<CapsuleCreateRequest>) -> Resp {
let mut rows = stmt.query(params![capsule_name]).unwrap();
match rows.next() {
Err(_) => return Resp::InternalServerError(json!({"message": "Database problem"})),
Ok(row_result) => match row_result {
None => println!("No matching row found"),
Some(_) => {
return Resp::BadRequest(
json!({"message": "A capsule with this name already exists"}),
)
Ok(row_result) => {
match row_result {
None => println!("No matching row found"),
Some(_) => return Resp::BadRequest(json!({"message": "A capsule with this name already exists"})),
}
},
}
};
// Generate an ID for both the new capsule and the public key entries
let capsule_id = Uuid::new_v4().to_string();
let key_id = Uuid::new_v4().to_string();
let access_token = match util::generate_access_token(&capsule_id) {
Err(_) => {
return Resp::InternalServerError(
json!({"message": "Unable to generate an access token"}),
)
}
let access_token = match generate_access_token(&capsule_id) {
Err(_) => return Resp::InternalServerError(json!({"message": "Unable to generate an access token"})),
Ok(x) => x,
};
@ -157,26 +184,45 @@ fn create_capsule(site: Json<CapsuleCreateRequest>) -> Resp {
// whitespace
if let Err(_) = conn.execute(
"INSERT INTO capsule (id, name, created) VALUES (?1, ?2, ?3)",
params![capsule_id, capsule_name, util::get_current_time()],
) {
return Resp::InternalServerError(json!({"message": "Database problem"}));
};
params![capsule_id, capsule_name, get_current_time()],
) { return Resp::InternalServerError(json!({"message": "Database problem"})); };
if let Err(_) = conn.execute(
"INSERT INTO token (id, capsule_id, token, created) VALUES (?1, ?2, ?3, ?4)",
params![key_id, capsule_id, &access_token, util::get_current_time()],
) {
return Resp::InternalServerError(json!({"message": "Database problem"}));
};
params![key_id, capsule_id, &access_token, get_current_time()],
) { return Resp::InternalServerError(json!({"message": "Database problem"})); };
if let Err(_) = conn.execute(
"INSERT INTO contact_detail (capsule_id, contact, created) VALUES (?1, ?2, ?3)",
params![capsule_id, site.contact_detail, util::get_current_time()],
) {
return Resp::InternalServerError(json!({"message": "Database problem"}));
};
params![capsule_id, site.contact_detail, get_current_time()],
) { return Resp::InternalServerError(json!({"message": "Database problem"})); };
return Resp::Ok(json!({"message": "Capsule created", "accessToken": access_token}));
}
#[derive(Debug)]
enum ApiTokenError {
BadCount,
Missing,
Invalid,
}
impl<'a, 'r> FromRequest<'a, 'r> for AuthenticatedCapsule {
type Error = ApiTokenError;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let keys: Vec<_> = request.headers().get("api-key").collect();
match keys.len() {
0 => Outcome::Failure((Status::BadRequest, ApiTokenError::Missing)),
1 => {
return match validate_access_token(keys[0]) {
Err(e) => {
println!("{}", e);
return Outcome::Failure((Status::BadRequest, ApiTokenError::Invalid));
},
Ok(auth_capsule) => Outcome::Success(auth_capsule),
};
},
_ => Outcome::Failure((Status::BadRequest, ApiTokenError::BadCount)),
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@ -184,103 +230,51 @@ struct CapsuleDeployRequest {
capsule_archive: String,
}
#[put("/capsule", data = "<capsule>")]
fn deploy_capsule(
capsule: Json<CapsuleDeployRequest>,
authenticated_capsule: util::AuthenticatedCapsule,
) -> Resp {
fn deploy_capsule(capsule: Json<CapsuleDeployRequest>, authenticated_capsule: AuthenticatedCapsule) -> Resp {
// Decode base64-encoded JSON capsule_archive. This gives a Vec<u8>
let archive_vec: Vec<u8> = match general_purpose::STANDARD.decode(&capsule.capsule_archive) {
let archive_vec: Vec<u8> = match base64::decode(&capsule.capsule_archive) {
Ok(x) => x,
Err(_) => return Resp::BadRequest(json!({"message": "Your capsule archive is not valid"})),
};
// Create a temporary directory and a file in that directory.
// We use this rather than tempfile because it makes it easier to keep the path around.
let tmp_dir = match tempdir() {
Ok(x) => x,
Err(_) => {
return Resp::InternalServerError(
json!({"message": "There was a problem deploying your capsule"}),
);
}
};
let path = tmp_dir.path().join(format!("{}_encodedcapsule.tar.gz", &authenticated_capsule.name));
// Write archive to disk
let mut path = String::from(".tar.gz");
path.insert_str(0, &authenticated_capsule.name);
path.insert_str(0, TMP_FILE);
let mut file = match File::create(&path) {
Ok(x) => x,
Err(_) => {
println!("Unable to create archive");
return Resp::InternalServerError(
json!({"message": "There was a problem deploying your capsule"}),
);
}
return Resp::InternalServerError(json!({"message": "There was a problem deploying your capsule"}));
},
};
// Write archive to disk
if let Err(_) = file.write_all(&archive_vec) {
println!("Unable to write to disk");
return Resp::InternalServerError(
json!({"message": "There was a problem deploying your capsule"}),
);
return Resp::InternalServerError(json!({"message": "There was a problem deploying your capsule"}));
};
// Load archive from disk and deflate, and write unzipped, unarchived files
let written_tar_gz = File::open(&path).unwrap();
let tar = GzDecoder::new(written_tar_gz);
let mut archive = Archive::new(tar);
if let Err(e) = archive.unpack(format!("{}/{}", util::get_env_var("CAPSULES_PATH"), &authenticated_capsule.name)) {
if let Err(e) = archive.unpack(format!("{}/{}", CAPSULE_DIR, &authenticated_capsule.name)) {
println!("{}", e);
println!("Unable to extract archive");
return Resp::BadRequest(json!({"message": "Unable to unpack your archive. Is it valid?"}));
return Resp::BadRequest(json!({"message": "Unable to unpack your archive. Is it valid?"}));
};
let url = format!("gemini://{}.{}", &authenticated_capsule.name, util::get_env_var("CAPSULE_DOMAIN"));
// Notify admins of the update
let _ = mail::send(&util::get_env_var("ADMIN_EMAIL"), "Capsule Published", &format!("A capsule has been updated: {}", &authenticated_capsule.name));
let url = format!("gemini://{}.{}", &authenticated_capsule.name, get_domain());
return Resp::Ok(json!({"message": "Your capsule was deployed", "url": url}));
}
#[derive(Serialize)]
struct CapsuleViewLog {
path: String,
timestamp: i32,
}
#[get("/logs")]
fn get_logs(authenticated_capsule: util::AuthenticatedCapsule) -> Resp {
let conn = match util::get_db() {
Ok(c) => c,
Err(_) => return Resp::InternalServerError(json!({"message": "Database problem"})),
};
let mut stmt = conn
.prepare("SELECT path, timestamp FROM view WHERE capsule_id = ?1 ORDER BY timestamp DESC LIMIT 100")
.unwrap();
let view_results = stmt
.query_map(params![authenticated_capsule.id], |row| {
Ok(CapsuleViewLog {
path: row.get(0)?,
timestamp: row.get(1)?,
})
})
.unwrap();
let views: Vec<CapsuleViewLog> = view_results.map(|view| view.unwrap()).collect();
return Resp::Ok(json!({"logs": &views}));
}
#[launch]
fn rocket() -> _ {
fn main() {
// Create needed directories
fs::create_dir_all(util::get_env_var("CAPSULES_PATH")).unwrap_or_default();
fs::create_dir_all(CAPSULE_DIR).unwrap_or_default();
rocket::build()
.configure(
Config::figment()
.merge(("port", 9000))
.merge(("address", IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))))
.merge(("limits", Limits::new().limit("json", 50.mebibytes()))),
)
.register("/", catchers![bad_request])
.mount(
"/",
routes![index, create_capsule, deploy_capsule, get_logs],
)
// Initialise and launch Rocket
let mut my_config = Config::active().unwrap();
my_config.set_port(9000);
my_config.set_limits(Limits::new().limit("json", 50 * 1024 * 1024)); // 50MB
let app = rocket::custom(my_config);
app.mount("/", routes![index, create_capsule, deploy_capsule]).launch();
}

View File

@ -1,120 +0,0 @@
use std::{
collections::HashSet,
env,
str,
time::{SystemTime, UNIX_EPOCH},
};
use rocket::{
self,
http::Status,
request::{self, FromRequest, Outcome, Request},
};
use serde::{Deserialize, Serialize};
use rusqlite::{params, Connection};
use jsonwebtoken::{self, decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
// Utility function: returns a u64 containing the current number of seconds since UNIX EPOCH
pub fn get_current_time() -> i64 {
return match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs() as i64,
Err(_) => 0 as i64,
};
}
// Utility function to get the named environment variable
pub fn get_env_var(key: &str) -> String {
return match env::var(key) {
Ok(x) => x,
Err(_) => String::from(""),
};
}
// Utility function to retrieve a connection to the database
pub fn get_db() -> Result<Connection, rusqlite::Error> {
return Ok(Connection::open(get_env_var("DB_PATH"))?);
}
// A struct to contain token data to embed in JWT tokens
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
}
#[derive(Debug)]
pub enum ApiTokenError {
BadCount,
Missing,
Invalid,
}
// A struct to represent a capsule derived automatically as a result of an authenticated request
pub struct AuthenticatedCapsule {
pub id: String,
pub name: String,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthenticatedCapsule {
type Error = ApiTokenError;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let keys: Vec<_> = request.headers().get("api-key").collect();
match keys.len() {
0 => Outcome::Error((Status::BadRequest, ApiTokenError::Missing)),
1 => match validate_access_token(keys[0]) {
Err(e) => {
println!("{}", e);
Outcome::Error((Status::BadRequest, ApiTokenError::Invalid))
}
Ok(auth_capsule) => Outcome::Success(auth_capsule),
},
_ => Outcome::Error((Status::BadRequest, ApiTokenError::BadCount)),
}
}
}
// Generate a JWT token, embedding the capsule ID as the token `sub`. Returns the token as a String.
pub fn generate_access_token(capsule_id: &str) -> Result<String> {
let my_claims = Claims {
sub: capsule_id.to_owned(),
};
let token = encode(
&Header::default(),
&my_claims,
&EncodingKey::from_secret(get_env_var("JWT_SECRET").as_ref()),
)?;
return Ok(token);
}
// Verify the access JWT token, and checks still valid in DB. Returns the authenticated capsule.
pub fn validate_access_token(token: &str) -> Result<AuthenticatedCapsule> {
// Validate the token integrity itself
let mut my_validation = Validation::new(Algorithm::HS256);
my_validation.validate_exp = false;
my_validation.required_spec_claims = HashSet::new();
let claims = decode::<Claims>(
&token,
&DecodingKey::from_secret(get_env_var("JWT_SECRET").as_ref()),
&my_validation,
)?
.claims;
// Verify the token in the DB and retrieve the capsule info
let conn = get_db()?;
let mut stmt = conn.prepare(
"SELECT capsule.id, name FROM capsule
INNER JOIN token ON capsule.id = token.capsule_id
WHERE capsule.id = ?1 AND token.token = ?2",
)?;
let mut rows = stmt.query(params![claims.sub, &token])?;
let first_row_result = rows.next()?;
if let Some(row) = first_row_result {
return Ok(AuthenticatedCapsule {
id: row.get(0)?,
name: row.get(1)?,
});
} else {
return Err(Box::from(rusqlite::Error::QueryReturnedNoRows));
}
}

1809
cli/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,19 @@
[package]
name = "captown"
version = "0.3.1"
version = "0.1.0"
authors = ["Will Webberley <me@wilw.dev>"]
edition = "2021"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
flate2 = "1.1.0"
tar = "0.4.44"
base64 = "0.22.1"
clap = { version = "4.5.31", features = ["derive"] }
dirs = "6.0.0"
tempfile = "3.17.1"
reqwest = { version = "0.12.12", features = ["json", "blocking"] }
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.139"
flate2 = "1.0.19"
tar = "0.4.30"
base64 = "0.13.0"
clap = "2.33.3"
dirs = "3.0.1"
tempfile = "3.2.0"
reqwest = { version = "0.11", features = ["json", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
chrono = "0.4.40"
prettytable = "0.10.0"

View File

@ -1,21 +1,17 @@
# capsule.town CLI client
## Run and develop
## Building
The CLI binary can be run locally simply using Cargo. When run locally, it's setup to communicate with a locally-running capsule.town API for development.
To cross-compile Linux:
For example:
```
cargo run logs
````
docker run --rm --user "$(id -u)":"$(id -g)" -v "$PWD":/usr/src/app -w /usr/src/app rust cargo build --release
```
## Compiling
## Release
Compile and deploy using Taskfile:
Upload to object storage:
```
task deploy-binary
3cmd put target/release/captown s3://capsuletown/cli/captown-linux-0.1 --acl-public
```
View the Taskfile in the project root for more information. Where cross-compilation is needed, currently we use Docker.

View File

@ -1,22 +1,13 @@
// Copyright (c) 2025, Will Webberley. See LICENSE
// Copyright (c) 2021, Will Webberley. See LICENSE
use base64::{engine::general_purpose, Engine as _};
use chrono::prelude::*;
use clap::{Parser, Args, Subcommand};
use dirs;
use flate2::{write::GzEncoder, Compression};
use prettytable::{row, Table};
use reqwest;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
env,
fs::{self, File},
io::{stdin, stdout, Write},
path::{Path, PathBuf},
str,
};
use std::{env, str, io::{stdin,stdout,Write}, collections::HashMap, fs::{self, File}, path::{Path, PathBuf}};
use clap::{Arg, App, SubCommand};
use flate2::{Compression, write::GzEncoder};
use serde::{Serialize, Deserialize};
use tempfile::tempdir;
use dirs;
use base64;
use reqwest;
const CONFIG_FILE: &str = ".capsules.yaml";
const DEFAULT_DEV_API_URL: &str = "http://localhost:9000";
@ -44,7 +35,7 @@ fn get_api_url() -> String {
} else {
String::from(DEFAULT_PROD_API_URL)
}
}
},
};
}
@ -62,17 +53,15 @@ fn read_directory_as_string() -> Result<String> {
tar.append_dir_all("", "capsule")?;
tar.into_inner()?;
let special_bytes = fs::read(file_path)?;
return Ok(general_purpose::STANDARD.encode(special_bytes));
return Ok(base64::encode(special_bytes));
}
// Utility function which prompts for user input. Returns a String.
fn prompt_for(prompt: &str) -> String {
let mut s = String::new();
let mut s=String::new();
print!("{}: ", prompt);
let _ = stdout().flush();
stdin()
.read_line(&mut s)
.expect("Unable to read input string.");
stdin().read_line(&mut s).expect("Unable to read input string.");
if let Some('\n') = s.chars().next_back() {
s.pop();
}
@ -93,7 +82,7 @@ fn home_path() -> Result<PathBuf, String> {
}
// Utility function to create a blank configuration file so future reads can be guaranteed
fn init_config() -> Result {
fn init_config() -> Result<> {
let mut config_path = home_path()?;
config_path.push(&CONFIG_FILE);
if let false = Path::new(&config_path).exists() {
@ -105,7 +94,7 @@ fn init_config() -> Result {
}
// Utility function to write the parameter configuration to disk, overwriting the existing one
fn write_config(config: &Config) -> Result {
fn write_config(config: &Config) -> Result<> {
let s = serde_yaml::to_string(config)?;
let mut config_path = home_path()?;
config_path.push(&CONFIG_FILE);
@ -132,9 +121,7 @@ fn read_config_capsule(name: &str) -> Result<ConfigCapsule> {
return Ok(capsule);
}
}
return Err(Box::from(
"Unable to find your capsule config in the configuration file.",
));
return Err(Box::from("Unable to find your capsule config in the configuration file."));
}
#[derive(Deserialize, Debug)]
@ -144,39 +131,32 @@ struct DeployResponse {
url: String,
}
// PUTs the base64-encoded capsule tarball to the server.
fn publish_capsule(capsule_name: String) -> Result<bool> {
let capsule_config = match read_config_capsule(&capsule_name) {
Ok(x) => x,
Err(x) => {
println!("{}", x);
return Err(Box::from("Unable to publish the capsule"));
}
fn publish_capsule(flag_capsule_name: Option<&str>) -> Result<bool>{
let capsule_name: String = match flag_capsule_name {
Some(x) => String::from(x),
None => prompt_for("Enter the name for this capsule"),
};
let capsule_config = read_config_capsule(&capsule_name).expect("The configuration for your specified capsule could not be found.");
let base64_encoded_capsule = match read_directory_as_string() {
Ok(x) => x,
Err(_) => {
return Err(Box::from("Unable to read capsule/ directory."));
}
},
};
let mut json_data = HashMap::new();
json_data.insert("capsuleArchive", &base64_encoded_capsule);
let client = reqwest::blocking::Client::new();
let res = client
.put(&format!("{}/capsule", get_api_url()))
let res = client.put(&format!("{}/capsule", get_api_url()))
.header("api-key", &capsule_config.access_token)
.json(&json_data)
.send()?;
.json(&json_data).send()?;
let status = res.status();
let response_data = res.json::<DeployResponse>()?;
if status.is_success() {
println!("Capsule published successfully.");
println!(
"Visit {} using a Gemini client to view your capsule.",
&response_data.url
);
println!("Visit {} using a Gemini client to view your capsule.", &response_data.url);
return Ok(true);
} else {
println!("Unable to publish your capsule: {}", &response_data.message);
@ -192,23 +172,18 @@ struct CreateResponse {
}
// Defines a new Gemini capsule by prompting for a capsule name and public key.
// POSTs these to the API.
fn create_capsule() -> Result<bool> {
fn create_capsule() -> Result<bool>{
let capsule_name: String = prompt_for("Enter a name for your new capsule");
let contact_details: String = prompt_for("OPTIONAL: Enter a contact identifier (e.g. email, Telegram, Matrix handle, etc.) for verification in case you lose your deployment access keys");
if let Ok(_) = read_config_capsule(&capsule_name) {
return Err(Box::from(
"A local configuration for this capsule name already exists.",
));
return Err(Box::from("A local configuration for this capsule name already exists."));
}
let mut json_data = HashMap::new();
json_data.insert("capsuleName", &capsule_name);
json_data.insert("contactDetail", &contact_details);
let client = reqwest::blocking::Client::new();
let res = client
.post(&format!("{}/capsule", get_api_url()))
.json(&json_data)
.send()?;
let res = client.post(&format!("{}/capsule", get_api_url())).json(&json_data).send()?;
let status = res.status();
let response_data = res.json::<CreateResponse>()?;
if status.is_success() {
@ -217,16 +192,11 @@ fn create_capsule() -> Result<bool> {
Some(x) => x,
};
let mut config = read_config()?;
let new_capsule = ConfigCapsule {
name: capsule_name,
access_token: access_token,
};
let new_capsule = ConfigCapsule { name: capsule_name, access_token: access_token };
config.capsules.insert(config.capsules.len(), new_capsule);
write_config(&config)?;
println!("\nYour capsule has been created!");
println!(
"\nYou can now run 'publish' to publish your gemfiles in the `capsule/` directory."
);
println!("\nYou can now run 'publish' to publish your gemfiles in the `capsule/` directory.");
println!("\nAn access key has been written to the config file. Remember to backup and keep this file safe so you can publish future updates!");
return Ok(true);
} else {
@ -235,109 +205,6 @@ fn create_capsule() -> Result<bool> {
}
}
#[derive(Deserialize, Debug)]
struct LogsResponse {
logs: Option<Vec<Log>>,
message: Option<String>,
}
#[derive(Deserialize, Debug)]
struct Log {
path: String,
timestamp: i64,
}
impl Log {
fn time(&self) -> String {
let datetime = DateTime::from_timestamp(self.timestamp, 0).unwrap();
let newdate = format!("{}", datetime.format("%Y-%m-%d %H:%M:%S"));
return newdate;
}
}
// Retrieves access logs for the specified capsule
fn capsule_logs(
capsule_name: String,
number_of_logs: usize,
) -> Result<bool> {
let capsule_config = match read_config_capsule(&capsule_name) {
Ok(x) => x,
Err(x) => {
println!("{}", x);
return Err(Box::from("Unable to get logs"));
}
};
let client = reqwest::blocking::Client::new();
let res = client
.get(&format!("{}/logs", get_api_url()))
.header("api-key", &capsule_config.access_token)
.send()?;
let status = res.status();
let response_data = res.json::<LogsResponse>()?;
if status.is_success() {
match response_data.logs {
None => println!("No logs found"),
Some(mut logs) => {
logs.truncate(number_of_logs);
logs.reverse();
let mut table = Table::new();
table.add_row(row!["TIME", "CAPSULE", "PATH"]);
for log in logs {
table.add_row(row![log.time(), capsule_name, log.path]);
}
table.printstd();
}
}
return Ok(true);
} else {
let error_message = match response_data.message {
None => String::from("Unknown error"),
Some(message) => message,
};
println!(
"Unable to get the logs for your capsule: {}",
&error_message
);
return Err(Box::from("Unable to get logs"));
}
}
// Clap CLI setup
#[derive(Parser)]
#[command(name = "Capsule.town (captown)")]
#[command(version)]
#[command(about = "Manage your capsules on capsule.town.", long_about = None)]
#[command(author = "Will W. (wilw.dev)")]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Create a new capsule on capsule.town
Create,
/// Publish the `capsule` directory to the specified capsule
Publish(PublishArgs),
/// Retrieve the access logs for a capsule
Logs(LogArgs),
}
#[derive(Args)]
struct PublishArgs {
/// Specify the capsule name to publish to
#[arg(short)]
capsule: Option<String>,
}
#[derive(Args)]
struct LogArgs {
/// Specify the capsule name to access
#[arg(short)]
capsule: Option<String>,
/// Specify the number of logs to retrieve
#[arg(short)]
number: Option<usize>,
}
fn main() {
// Setup config
@ -345,35 +212,35 @@ fn main() {
println!("{}", e);
}
let cli = Cli::parse();
// Read command-line commands/args
let app_args = App::new("Capsule.Town")
.version("0.1")
.author("Will W. (wilw.dev)")
.about("Publishes your capsule to capsule.town")
.subcommand(SubCommand::with_name("create")
.about("Creates a new capsule"))
.subcommand(SubCommand::with_name("publish")
.about("Publishes your capsule")
.arg(Arg::with_name("capsule")
.short("c")
.long("capsule")
.takes_value(true)
.help("Name of the capsule to be published")));
let matches = app_args.get_matches();
if let Err(err) = match &cli.command {
Commands::Create => {
create_capsule()
}
Commands::Publish(args) => {
let capsule_name: String = match &args.capsule {
Some(x) => String::from(x),
None => prompt_for("Enter the name for this capsule"),
};
// Handle commands
if let Err(err) = match matches.subcommand() {
("create", Some(_)) => create_capsule(),
("publish", Some(sub_match)) => {
let capsule_name = sub_match.value_of("capsule");
publish_capsule(capsule_name)
},
Commands::Logs(args) => {
let capsule_name: String = match &args.capsule {
Some(x) => String::from(x),
None => prompt_for("Which capsule do you want to view logs for?"),
};
let mut number_of_logs: usize = match args.number {
Some(x) => x,
None => 20,
};
if number_of_logs > 200 {
number_of_logs = 200;
}
capsule_logs(capsule_name, number_of_logs)
_ => {
println!("Unknown command. Available commands: create, publish");
Err(Box::from("Unable to continue"))
},
} {
println!("Exiting with error: {}", err);
println!("Program is exiting with error. {}", err);
std::process::exit(1);
}
}

View File

@ -1,12 +1,3 @@
# capsule.town home
This directory contains the Gemini files that form the gemini://capsule.town homepage.
## Deploy changes
New changes are deployed using the capsule.town CLI itself, captown. For convenience, use the included Taskfile command:
```
task deploy-root-capsule
```

View File

@ -17,8 +17,11 @@
🌆 capsule.town is a safe and free place for hobbyists, gem-loggers, and everyone else to host their static Gemini capsules and gemlogs.
✨ No sign-up is required and your capsule can be live within seconds.
✨ You get a free part of Geminispace hosted at <yoursubdomain>.capsule.town.
✨ You get a free Geminispace hosted at <yoursubdomain>.capsule.town.
✨ You can publish entirely anonymously.
✨ There is no logging, tracking, or analytics.
🚧 This project is currently in open beta. Feedback, bug-reports, and contributions are welcome. Please see below for ways to get in touch.
=> gemini://capsule.town/start.gmi Get started
@ -28,7 +31,7 @@ This project is open-source, and contributions are welcome. Likewise, you are fr
## Why host your content on capsule.town?
capsule.town is free and aims to offer a convenient space for people that would like a Gemini capsule but don't want to or can't host it themselves.
capsule.town is free and aims to offer a convenient space for people that would like a Geminispace but don't want to or can't host it themselves.
Think of this service as the Gemini version of static site hosting services like Netlify or Vercel. You can publish your capsule at any time using a simple client without needing to worry about provisioning and maintaining servers.
@ -36,9 +39,9 @@ Think of this service as the Gemini version of static site hosting services like
## Why is it free?
capsule.town does not generate any income, and I pay for the servers and upkeep myself.
capsule.town does not generate any income, and I pay for the servers/upkeep myself.
This is one way I can give back to the indie and open-source community. Your data is not sold.
This is one way I can give back to the indie and open-source community. There is no logging or analytics and your data is not collected or sold. You can view and evaluate the source yourself (there is a link above).
By taking part you are certainly not the product; you are a member of the community.
@ -54,12 +57,3 @@ We know that this type of service isn't for everybody. If you want a Geminispace
Check out my own capsule for ways to get in touch:
=> gemini://wilw.capsule.town My Capsule
## Terms and policies
=> gemini://capsule.town/privacy.gmi capsule.town Privacy Policy
=> gemini://capsule.town/terms.gmi capsule.town Terms of Use
## Online safety
=> gemini://capsule.town/online-safety Find out about online safety on capsule.town

View File

@ -1,46 +0,0 @@
# Online safety on capsule.town
=> gemini://capsule.town Return home
# Online Safety Portal
**Important: this portal refers specifically to the version of Capsule Town running at capsule.town (and subdomains). We refer to this as "the service" or often simply just "Capsule Town".** Capsule Town is open-source software and can be run by anyone and be called anything. Other "instances" of "Capsule Town" should have their own online safety notice, risk assessment, and/or any other required documentation and processes in place that are suitable to that instance.
## Complaints
If you would like to complain about online safety on Capsule Town, our policies, or how we may have treated a previous request or complaint from you, then please do so using our dedicated reports and complaints email address: reports@capsule.town.
The Gemini protocol does not readily support form submissions in the way that HTTP/HTML does. As such, currently all reports and complaints must be made via this email address.
## About
The concept of online safety is hugely important to small communities like Capsule Town. By "online safety", we refer to the ability for people to use our services without the fear of being abused or harassed, and without people being exposed to harmful or abusive content.
Capsule Town uses the UK Government's Online Safety Act as a guide and framework for our processes.
Capsule Town is a platform that is partly based on the concept of user-generated content. As such, it is a "U2U" service (user-to-user) under the Online Safety Act. This also means that we have legal duties to ensure that we comply with the act and ensure that the risk of harms related to online safety are minimised.
## Policy
Capsule Town's Online Safety Policy governs our approach to online safety, along with our responsibilities to illegal content risk assessments and other related factors.
=> gemini://capsule.town/online-safety/policy.gmi View the Online Safety Policy
## Accountability
The named individual responsible for online safety duties (including the handling of reporting and complaints) for Capsule Town is Will Webberley.
## Online Safety Risk
Using the guidance information available from Ofcom, we assess that:
- Capsule Town is a "smaller service" because it has fewer than 7 million monthly UK active users.
- Capsule Town is "low risk" as a result of our risk assessment against the 17 kinds of priority illegal content listed in the Online Safety Act.
We regularly review and make updates to our Illegal Content Risk Assessment, in accordance with our Online Safety Policy. For transparency, our most recent assessments, including the determination and implementation of mitigating measures, are publicly available and can be found below.
## Recent Illegal Content Risk Assessments (including mitigation measures)
=> gemini://capsule.town/online-safety/risk-assessment-v202501.gmi v2025.01 - Initial Illegal Content Risk Assessment
=> gemini://capsule.town Return home

View File

@ -1,107 +0,0 @@
# capsule.town Online Safety Policy
=> gemini://capsule.town Return home
# Online Safety Policy
This policy is designed to be accessible, understandable, and easy to read without legal and other jargon. If you have any comments, questions, or concerns about this policy, please get in touch with us by emailing hello@capsule.town.
This policy will have slight changes made to it occasionally. Please refer back to it from time to time.
**Important: this policy refers specifically to the version of Capsule Town running at capsule.town (and subdomains). We refer to this as "the service" or often simply just "Capsule Town", "us", "we", etc.** Capsule Town is open-source software and can be run by anyone and be called anything. Other "instances" of "Capsule Town" should have their own online safety notice, policy, risk assessment, and/or any other required documentation and processes in place that are suitable to that instance.
This policy governs Capsule Town's responsibilities with regard to online safety for all its users and visitors of the service.
## Complaints
If you would like to make a complaint about this policy, or Capsule Town's handling of online safety in general, please reach out to us using the dedicated reports and complaints email address: reports@capsule.town.
## Responsible Person
In accordance with the UK Online Safety Act, we have identified that the named individual responsible for online safety duties (including the handling of reporting and complaints and management of online safety processes, including this policy) for Capsule Town is Will Webberley, who can be contacted using the email address at the top of this policy document.
## Illegal Content Risk Assessments
Capsule Town will at all times maintain a valid and up-to-date Illegal Content Risk Assessment, which assesses the risk of illegal content being encountered on Capsule Town, Capsule Town being used to facilitate a related offence (under the Online Safety Act) and the determination and implementation of proportionate mitigation measures.
This assessment, where possible, will be published online such that it is publicly available for transparency purposes for our visitors and users. For more information, and to view our risk assessments, please refer to the Capsule Town Online Safety Portal.
=> gemini://capsule.town/online-safety Capsule Town Online Safety Portal
This policy governs that the risk assessment will be reviewed according to the following schedule:
- At least once per year;
- If substantial changes are made to Capsule Town (i.e. changes that would affect a user's risk with regard to online safety);
- If any legal regulator related to online safety (e.g. Ofcom) publish updated guidance or risk profiles that need to be considered.
Upon each review, we expect the latest risk assessment to be published within one month of the new assessment being completed.
Upon each review, identified recommendations for service changes (for example, from relevant Codes of Practice) will be implemented in a timely manner and, where possible, within two months of the review. If the change impacts the risk assessment, the current active assessment will be updated to reflect the change in risk.
Upon each review, changes will be communicated throughout Capsule Town's governance channels to ensure awareness of practice and risk is maintained.
## Moderation
Where possible, user-generated content (including all Gemini capsules) is to be moderated by Capsule Town administrators to reduce the likelihood of dissemination of such content to other visitors and users.
This includes via the following mechanisms:
- Post-publish moderation (content "review"): where content must be reviewed in a timely manner (we do not define this timeframe, but it must be "reasonable" and typically within one business day) once it is available to other users. Admins are automatically notified of such publish update events to reduce the time in moderation activity.
Where it is not possible, and in all cases, provide a reporting or complaints mechanism (as described in the relevant section below in this policy).
If, during the moderation process, suspected illegal content is found, a manual process is invoked to remove the content. This is achieved by removing files and updating database entries accordingly. This must be carried out immediately upon discovering such content.
If illegal content is identified, we may notify the responsible user of the content take-down and/or alert the relevant authorities, depending on the severity of the content.
When extending Capsule Town with new features, or updating existing ones, it is required that consideration is given to developing software such that content approval is built-in to the changing feature to ensure Capsule Town maintains or continuously improves its online safety features.
## Individuals' Rights and Freedoms
Capsule Town respects the rights and freedoms of individuals using the service, which includes an individual's right to expression. In the handling of complaints and moderation, we will be diligent in considering these rights.
Once judgment has been made, we will make the appropriate action. Complaints or appeals about this action can subsequently be made via Capsule Town's reporting and complaints process (see below).
## Reporting and Complaints
Capsule Town must, at all times, maintain a robust, transparent, easy-to-use, and available reporting and complaints procedure for at least the following use cases:
- Complaining about Capsule Town's online safety processes and practices (including this policy and risk assessments);
- Reporting user-generated content that is illegal (or, in some cases, purely offensive);
- Reporting cases where Capsule Town is being used to facilitate or commit a legal offence (particularly those related to online safety);
- Complaining about decisions taken by Capsule Town with relation to user-generated content or the handling of other reports or complaints (for example, perceived unfair treatment);
- Appeals to complaints made about user-generated content or to any other reports or complaints made.
Capsule Town facilitates reports and complaints via a dedicated email address available to all visitors and users: reports@capsule.town.
Unlike HTTP/HTML, the Gemini protocol does not readily support form or data submissions and so email is currently the only supported mechanism for reports and complaints on Capsule Town.
The report and complaints process is available to both registered users and visitors and it must be prominently displayed and accessible from key areas of the Capsule Town home capsule such that it is readily available to users and visitors.
Messages to this email address immediately notify Capsule Town admins, who will then take appropriate action accordingly at a high priority (within a 3 work hours UK time). Actions taken, or not taken, as a result of complaints may be communicated back to the complainer/reporter, as described in this policy.
If the complaint is an appeal for which a decision related to content or capsule removal is reversed, the admins will allow the user to re-register the capsule and re-add the content in its original form and the complainer will be notified. Capsule Town will not be able to automatically reinstate content or capsules that have been deleted as the result of a suspected illegal content takedown. If it becomes apparent that a disproportionate number of takedown decisions are reversed, changes will be made to the original decision process to ensure that these are more accurate in future.
If the complaint is related to the approval or moderation process too aggressively removing or de-prioritising content, we will review this against the Terms of Use and illegal content guidelines to determine if the original decision in the approval/moderation needs to be reversed. If so, the complainer will be notified and alerted to any other available actions.
If the complaint is not an appeal, but is determined to be manifestly unfound, the complaint will not be dealt with. Capsule Town may or may not notify the complainer. For example, complaints are unfound if they do not refer to illegal content, or do not refer to content that is forbidden (as described in our Terms of Use). This policy will be reviewed annually to ensure that legitimate complaints are not being identified as unfound.
All complaints, appeals, and reports (no matter the cause or reason) will be dealt with in a reasonable time-frame, and usually within one working day. Higher-priority complaints (e.g. those related to suspected illegal content) will be dealt with within a few working UK hours.
All complaints, appeals, and reports (even if unfound) will be recorded in a register, alongside actions taken and justifications. This will be used to help in reviewing this policy.
Generally, whilst respecting user freedom of expression to appropriate points, Capsule Town admins will be the sole deciders when it comes to handling content and capsule take downs, complaints, and appeals.
Capsule Town will make sure to regularly test the reports and complaints process to ensure continuous availability.
## Proscribed Organisations
Users and content from or related to proscribed organisations (organisations banned under the UK Terrorism Act) is forbidden, and will be removed if encountered or a relevant complaint is made. For example, this could be a capsule name, link to a proscribed organisation website, or other content uploaded as part of a capsule.
## User and Visitor Responsibilities
Individuals visiting or using Capsule Town ("users") have both community (e.g. related to service "terms of use") and legal responsibilities related to online safety. In particular:
- Users must not use Capsule Town to facilitate or commit any legal offences (under UK law) and -- specifically with regard to this policy -- offences related to illegal content or harms related to the Online Safety Act.
- Users must not use Capsule Town to upload, create or otherwise store, process, or disseminate any form of illegal content as described in the UK Online Safety Act.
- Users must not use Capsule Town in any way that brings harm to any other user or to the service itself.
- Using the dedicated reports and complaints process, users must report any occurrences of (or suspicions of) illegal content encountered on Capsule Town, or cases where it is suspected that Capsule Town is being used to facilitate or commit a legal offence.
=> gemini://capsule.town Return home

File diff suppressed because it is too large Load Diff

View File

@ -1,129 +0,0 @@
# capsule.town: Privacy Policy
=> gemini://capsule.town Return home
# Privacy Policy
This policy is designed to be accessible, understandable, and easy to read without legal and other jargon. If you have any comments, questions, or concerns about this policy, please get in touch with us by emailing hello@capsule.town.
This document will have slight changes made to it occasionally. Please refer back to it from time to time.
**Important: this policy refers specifically to the version of Capsule Town running at capsule.town (and subdomains). We refer to this as "the service" or often simply just "Capsule Town", "us", "we", etc.** Capsule Town is open-source software and can be run by anyone and be called anything. Other "instances" of "Capsule Town" should have their own privacy policy in place that is suitable to that instance.
This policy governs the use and protection of personal data of people (users, you, etc.) using Capsule Town.
Data protection refers to the responsible security of personal data and transparency in the way we handle and process such data. Personal data is information that - on its own or in conjunction with other data - can be used to identify an individual person. With respect to the UK General Data Protection Regulation (GDPR), Capsule Town acts as data controller for the data you provide through using our services.
## Complaints
If you would like to complain about this policy, or how we may have treated a request from you with respect to data protection, then please get in touch with us in the first case so that we can help rectify the problem. In other cases, you may also want to get in touch with the Information Commissioners Office (ICO), who may be able to provide you with more information and support. Their website is at [https://ico.org.uk](https://ico.org.uk).
## When does Capsule Town collect data, and what data does it collect?
### When visiting our main capsule
When you navigate to our main capsule using a Gemini browser, your visit is logged by the server but we don't collect any data about you or your device.
### When sending us an email
Sometimes you may wish to send an email to us or reply to an email we have sent you. Any emails received will be treated in confidence and kept securely. Strong passwords and multi-factor authentication is implemented on all email accounts that can receive such emails.
In these cases, we will process:
- Your email address
- Any other information you include in your email headers or body (e.g. your name)
### When registering a new capsule
Capsule Town allows its users to register a new capsule, which is achieved either via the HTTP API directly or by using the Capsule Town CLI tool. At this stage, we ask you for the name of the capsule being created (which forms part of its address when published) and optional contact details, which can be used to help recover your access credentials if you lose them.
In these cases, we will process:
- The name of your capsule
- Any supplied contact details you optionally provide (e.g. an email address or Telegram handle)
### When publishing your capsule
Once you've registered a capsule, you may wish to publish your Gemini files to it, such that they can then be viewed by other people browsing your capsule. If you do so, we process the Gemini files you supply as part of the request or CLI command.
In these cases, we may process:
- Any Gemini content present in your capsule files
## Who has access to your data?
Staff operating Capsule Town can view capsules and their data.
Other users and visitors to your capsule (including the public) will have access to the Gemini files you uploaded when publishing your capsule.
In order to provide access to our services to users, we also sometimes need to pass pieces of your personal data to third-party services (known as ' data processors' or 'subprocessors' for the purposes of the GDPR). We only ever do this when this is directly related to providing the service to you, and we only send the minimum amount of information required. We ensure that the processors' own privacy policies follow suitable data protection practices. Our current data processors are:
- Backblaze (for backing-up all Capsule Town data)
- All data is encrypted by Capsule Town before being sent to Backblaze.
Capsule Town runs on Linode servers.
## How long do we keep your data for?
We keep your capsule data for as long as it is active. You can remove the data at any time by publishing your capsule with empty file contents. To fully delete or rename the capsule itself, please reach out to us by email.
Please note that data held in backup systems may be stored for up to an additional 30 days after content or capsules is deleted.
## Where is your data stored?
Our databases and servers are based in the UK, and so your data will primarily be stored and processed within the UK. We use Backblaze's EU servers for our backups.
## How do we protect your data?
All data is encrypted during transmission (e.g. between your device and our servers, and between our servers), and when stored ("encrypted at rest"). Our servers are well-protected with industry standard security measures.
## Cookies
Capsule Town runs on Gemini and therefore does not use or store cookies on your computer. Using the CLI tool will automatically create a small file in your computer's home directory in order to store the credentials that allow you to publish your capsule.
## Child safety
Children under the age of 16 are not allowed to use Capsule Town or to provide us with personal data. As such, we do not knowingly store or process personal data relating to children under the age of 16.
If a user account or content is created and suspected to be originated from a child, it may be removed.
## Your rights
We take the handling of personal data very seriously, and we want to make sure that you are aware of your rights under this policy. If your wish to invoke your rights requires us to complete some action on your behalf (for example, to stop processing your data), then we will always deal with your request in total confidence, at no cost, and as soon as we can (within 30 days of receiving your request).
### Right to be informed
You have a right to know about how we handle and process your personal data. This Privacy Policy aims to fulfil this Right, but please email us if you have further questions or concerns.
### Right of access
You have a right to know if we store or process your personal data and to obtain access to the personal data about you that we, or any data processors that process data on our behalf, have about you. To obtain this information, please email us.
### Right to rectification
You have a right to have personal data we keep or process about you rectified. If data we have about you is incorrect or incomplete, then please email us with details of any corrections to be made.
### Right to erasure
You have the right to have all of your personal data erased, which will prevent any further storage or processing any of your personal data on our behalf, and will sometimes result in a necessary deletion of any accounts you hold with us. Please email us with details of your request.
### Right to restrict processing
You have the right to halt the processing of your personal data in the way that you choose. For example, you may wish to maintain a capsule with us but no longer want us to use one of our data processors to process your data. To restrict the processing of your personal data, please email us with details of your request.
Please note that in some cases it may not be possible to restrict processing whilst still providing services to you.
### Right to data portability
You have the right to obtain personal data we have or process about you in a format that is useful to you for the purposes of portability. We can provide data to you in the following formats:
- CSV
- JSON
Please email us with details of your request.
### Right to object
You have a right to object to the processing of your personal data in particular ways. For example, for marketing or profiling purposes. If you would like to object to our processing of your data, then please email us.
### Rights related to automated decision making including profiling
We do not use personal data for automated decision making, and do not use such data for profiling users. Additionally, any processing done for analytics and reporting is done on an entirely anonymous basis. For more information or if you have any concerns, please email us.
=> gemini://capsule.town Return home

View File

@ -2,7 +2,7 @@
=> gemini://capsule.town Return home
This page describes the rules applying to all capsules hosted on capsule.town.
This page describes the Rules applying to all capsules hosted on capsule.town.
By creating and publishing your capsule, you declare that you agree to these rules.
@ -10,11 +10,6 @@ Please note that capsule.town offers no warranty/SLA or anything else of this ty
The admins and owners are not responsible for the content generated and posted by its users.
## Privacy & Terms
=> gemini://capsule.town/privacy.gmi View the capsule.town Privacy Policy
=> gemini://capsule.town/terms.gmi View the capsule.town Terms of Use
## Rules
capsule.town aims to be a safe space for everyone. Its purpose is to provide a space for people to host constructive, useful content, personal items, portfolios, and so on.

View File

@ -4,6 +4,8 @@
🚀 We're excited for you to join capsule.town!
🚧 Please note that this service is currently in early beta. Please get in touch if you have any feedback, bug reports, or would like to contribute.
## Intro
Publishing your capsule is handled through a HTTP API, however we recommend most people make use of our official wrapper client, which is described below.
@ -16,27 +18,28 @@ Publishing your capsule is handled through a HTTP API, however we recommend most
We recommend most people download and make use of the capsule.town client, called `captown`, for publishing their capsule. However, if you'd rather use your own tooling (cURL, or something else) you can interface directly with the service using HTTPS (documentation for this is coming soon).
The current version of the client is: 0.1. Replace the version numbers in commands below to get the latest version.
You can use CURL to get the client, as described below.
For Linux (amd64):
For Linux:
```
➡️ curl https://download.capsule.town/captown-linux-0.3 -o captown
➡️ curl -O https://download.capsule.town/captown-linux-0.1
```
For Mac (arm64):
For Mac:
```
➡️ curl https://download.capsule.town/captown-mac-0.3 -o captown
➡️ curl -O https://download.capsule.town/captown-mac-0.1
```
If you need a binary for an alternative architecture, please email us on hello@capsule.town
We do not yet have a Windows version, but contributions are welcome.
The Windows client is coming soon (contributions welcome).
Once downloaded:
1. Mark the binary as executable: `chmod +x captown`
2. Move the binary to a location of your choosing: e.g. `sudo mv captown /usr/local/bin`.
1. Mark the binary as executable: `chmod +x captown-mac-0.1`
2. Move the binary to a location of your choosing: e.g. `mv captown-mac-0.1 /usr/local/bin/captown`.
The rest of this guide assumes the binary is somewhere on your `PATH` and can be invoked using `captown` (as above).
@ -65,14 +68,9 @@ Create a new file called `capsule/index.gmi` and write a welcome message inside:
### 3. Create your capsule
By creating and publishing your capsule on capsule.town, you indicate that you have read and agree to the capsule.town Privacy Policy, Terms of Use, and the general rules, as listed below.
By creating and publishing your capsule on capsule.town, you agree to the capsule.town rules.
=> gemini://capsule.town/rules.gmi Check out the general rules
=> gemini://capsule.town/privacy.gmi View the Privacy Policy
=> gemini://capsule.town/terms.gmi View the Terms of Use
=> gemini://capsule.town/online-safety Find out about online safety on capsule.town
=> gemini://capsule.town/rules.gmi Check out the rules
The next step is to create your new capsule on capsule.town. To do so, run:
@ -105,16 +103,6 @@ Whichever route you choose, if all goes well you'll then be able to visit `gemin
🗒 Note: the `publish` command must be run from the root of your project (i.e. the directory containing the `capsule/` sub-directory).
### 5. View your capsule's logs
You can see an access log of your capsule by running the `logs` command:
```
➡️ captown logs -c mycapsule
```
View other options for this command by running `captown help logs`.
## Making updates
When you've made updates that you want to publish, simply run `captown publish` again to make them live.

View File

@ -1,72 +0,0 @@
# capsule.town: Terms of Use
=> gemini://capsule.town Return home
# Terms of Use
This document is designed to be accessible, understandable, and easy to read without legal and other jargon. If you have any comments, questions, or concerns about this document, please get in touch with us by emailing hello@capsule.town.
This document will have slight changes made to it occasionally. Please refer back to it from time to time.
**Important: this document refers specifically to the version of Capsule Town running at capsule.town (and subdomains). We refer to this as "the service" or often simply just "Capsule Town", "us", "we", etc.** Capsule Town is open-source software and can be run by anyone and be called anything. Other "instances" of "Capsule Town" should have their own terms of Use in place that are suitable to that instance.
## Liability
Capsule Town does not guarantee constant availability of service access and accepts no liability for downtime or access failure due to circumstances beyond its reasonable control (including any failure by ISP or system provider).
No data transmission over the Internet can be guaranteed as totally secure. Whilst we strive to protect such information, we do not warrant and cannot ensure the security of information which you transmit to us. Accordingly, any information which you transmit to us is transmitted at your own risk.
Any information on our services may include technical inaccuracies or typographical errors. We strive to maintain accuracy as much as possible.
Our services are provided on an “as is”, “as available” basis without warranties of any kind, express or implied, including, but not limited to, those of TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE or NON-INFRINGEMENT or any warranty arising from a course of dealing, usage, or trade practice. No advice or written information provided shall create a warranty; nor shall members or visitors to our services rely on any such information or advice.
## Capsule Credentials
You must keep any credentials you use with Capsule Town safe and secure. We recommend backing them up (e.g. with a password manager), but do not share your credentials or give them to other people. If your credentials are compromised, other people may be able to update your capsule.
You are free to use anonymous information (e.g. your email or Telegram handle) when creating your capsule.
## User Generated Content
We distribute the Gemini content supplied by our users. We try to maintain a safe platform for all of our users, but cannot take responsibility for such content. We are not liable for any damages as a result of such content. Self-policing is a an important feature of platforms like Capsule Town, so please report any problems with such user-generated content to the email address above. We accept no liability or responsibility to any person or organisation as a consequence of any reliance upon the information contained by our services.
The services may contain links to other Gemini pages or websites on the internet provided by users. Whilst we strive to ensure user content is kept safe and legal (in accordance with our Online Safety Policy) we are not responsible for the accuracy, legality, decency of material or copyright compliance of any such linked websites or services or information provided via any such link.
We reserve the right to permanently ban any user from our services for any reason related to mis-behaviour (including actions taken on Capsule Town). We will be the sole judge of "behaviour" and we do not offer appeals in those cases. We also reserve the right to remove any content at any time and for any reason, if we assess it to be forbidden content (see below).
The following types of content are forbidden, and may result in capsule or content takedowns:
- Harmful or illegal content (including illegal content as described in the UK Online Safety Act).
- Abusive content (e.g. information that aims to harm or bring harm to another user). Harm includes emotional harm.
- Harassing content (e.g being overly personal publicly about yourself or others in a harassing way).
- Content that aims to mimic or impersonate someone else or another organisation.
## Code of Conduct
All users must follow the Capsule Town code of conduct:
- Do not upload or add content that contains slurs, racist, homophobic, transphobic, ableist or otherwise discriminatory content. Do not add or upload content containing any hateful ideologies.
- Be kind to others, and don't be intentionally difficult or antagonistic. Harassment or threats or personal attacks will not be tolerated in any way.
- Do not post any illegal (from the perspective of UK law) or copyrighted content for which you do not own the rights, or links to such content.
- Do not post any content, or interact with the platform, in a way that is designed to bring harm to the platform or to negatively affect other people.
- Do report content from others that you suspect of being harmful, abusive, or illegal.
## Moderation and Complaints
All types of content (i.e. Gemini capsules) are subject to *moderation* and are continually monitored and reviewed.
If, as a result of such moderation, we determine that content is harmful, illegal (including illegal content as described in the UK Online Safety Act), or otherwise forbidden or unsuitable, we will delete the content and/or capsule.
If a user encounters content they deem to be illegal, unsuitable or otherwise harmful, they must report this to the Capsule Town admins using the email address reports@capsule.town.
The nature of Capsule Town's service (which is similar to a website builder) is such that we don't include links to report forms or other information from within the capsules (akin to websites) of our users. Equally, the Gemini protocol does not facilitate the submission of form data like HTML/HTTP does. As such, email is currently the only way for reports or complaints to be made on Capsule Town.
Please note that manifestly unfounded complaints will be rejected and no action will be taken.
If, as a result of complaint/report or moderation, we determine that your content is unsuitable, or that the content is associated with a proscribed organisation (under the UK Terrorism Act), it will be immediately deleted from Capsule Town. Depending on the severity of the content, you may not be notified about this (even if we have your contact details), but we may notify relevant authorities.
If you have a complaint or appeal about our handling of online safety duties or about capsule or content takedowns, you may submit a complaint or appeal using the same email address.
In handling all moderation, complaints, and appeals, we take into account freedom of expression.
=> gemini://capsule.town Return home

117
server/Cargo.lock generated
View File

@ -1,7 +1,5 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "agate"
version = "2.3.0"
@ -12,24 +10,12 @@ dependencies = [
"mime_guess",
"once_cell",
"percent-encoding",
"rusqlite",
"rustls",
"tokio",
"tokio-rustls",
"url",
]
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -53,12 +39,6 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
[[package]]
name = "bumpalo"
version = "3.4.0"
@ -101,18 +81,6 @@ dependencies = [
"termcolor",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "form_urlencoded"
version = "1.0.0"
@ -132,35 +100,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
dependencies = [
"hashbrown",
]
[[package]]
name = "hermit-abi"
version = "0.1.17"
@ -204,19 +143,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.142"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [
"pkg-config",
"vcpkg",
]
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
[[package]]
name = "log"
@ -299,9 +228,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.17.1"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
[[package]]
name = "percent-encoding"
@ -315,12 +244,6 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827"
[[package]]
name = "pkg-config"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "proc-macro2"
version = "1.0.24"
@ -354,20 +277,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "rusqlite"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustls"
version = "0.19.0"
@ -391,12 +300,6 @@ dependencies = [
"untrusted",
]
[[package]]
name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.3.19"
@ -532,24 +435,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.69"

View File

@ -21,7 +21,6 @@ once_cell = "1.4"
percent-encoding = "2.1"
rustls = "0.19.0"
url = "2.1"
rusqlite = "0.29.0"
[profile.release]
lto = true

View File

@ -14,7 +14,6 @@ use {
net::SocketAddr,
path::Path,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
},
tokio::{
io::{AsyncReadExt, AsyncWriteExt},
@ -23,16 +22,8 @@ use {
},
tokio_rustls::{TlsAcceptor, server::TlsStream},
url::{Host, Url},
rusqlite::{Connection},
};
const DB_FILE: &str = "/usr/local/var/capsules_db/db.db3";
// Utility function to retrieve a connection to the database
fn get_db() -> Result<Connection, rusqlite::Error> {
return Ok(Connection::open(DB_FILE)?);
}
fn main() -> Result {
if !ARGS.silent {
env_logger::Builder::new().parse_filters("info").init();
@ -252,7 +243,7 @@ async fn send_response(url: Url, stream: &mut TlsStream<TcpStream>) -> Result {
}
}
}
// Make sure the file opens successfully before sending the success header.
let mut file = match tokio::fs::File::open(&path).await {
Ok(file) => file,
@ -272,26 +263,6 @@ async fn send_response(url: Url, stream: &mut TlsStream<TcpStream>) -> Result {
// Send body.
tokio::io::copy(&mut file, stream).await?;
// Capture the view
#[derive(Debug)]
struct Capsule {
id: String,
}
let conn = get_db().unwrap();
let mut stmt = conn.prepare(&format!("SELECT id, name FROM capsule WHERE name = '{}'", &subdomain))?;
let mut capsule_cursor = stmt.query_map([], |row| {
Ok(Capsule {
id: row.get(0)?,
})
})?;
let capsule = capsule_cursor.next().unwrap()?;
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
_ = conn.execute("CREATE TABLE IF NOT EXISTS view (capsule_id TEXT, path TEXT, timestamp INTEGER)", ());
conn.execute("INSERT INTO view (capsule_id, path, timestamp) VALUES (?1, ?2, ?3)",
(&capsule.id, &url.path(), timestamp),
)?;
Ok(())
}