git-gemini-forge 0.6.6

A simple Gemini server that serves a read-only view of public repositories from a Git forge.
pub mod net;
pub mod robotstxt;

pub mod error;
pub mod responses;

// Forge APIs:
pub mod forgejo;
pub mod gitlab;

use crate::{
	config::SecretHeader,
	handlers::templates::PercentEncoded,
	network::{forgejo::ForgejoApi, gitlab::GitLabApi},
};
use async_trait::async_trait;
use net::Net;
use reqwest::header::{AUTHORIZATION, HeaderName};
use std::fmt::Debug;
use strum_macros::{Display, EnumIter, EnumString};
use url::Url;

// Describe the interfaces we expect:
#[derive(Clone, Copy, Debug, Display, EnumIter, EnumString, PartialEq)]
#[strum(ascii_case_insensitive)] // See https://docs.rs/strum/latest/strum/additional_attributes/index.html
pub enum ForgeApiKind {
	Forgejo,
	Gitea,
	GitLab,
}

/// Describes an interface by which we expect to get certain details
/// from a git forge. Implementations usually make REST calls via HTTP.
#[async_trait]
pub trait ForgeApi
where
	Self: Sync + Send + Sized,
{
	/// The kind of server software that serves this API.
	fn kind(&self) -> ForgeApiKind;

	/// The URL at which to communicate with the API. (e.g. "https://git.average.name/api/v1/")
	fn base_url(&self) -> &Url;

	/// Retrieves branches for the given repository.
	async fn get_branches(
		&self,
		net: &Net,
		username: &PercentEncoded,
		repo_name: &PercentEncoded,
	) -> Result<Vec<responses::Branch>, error::Error>;

	/// Retrieves the file or list of files at the given path.
	async fn get_contents(
		&self,
		net: &Net,
		username: &PercentEncoded,
		repo_name: &PercentEncoded,
		branch_name: &PercentEncoded,
		item_path: &PercentEncoded,
	) -> Result<responses::Contents<responses::Item>, error::Error>;

	/// Retrieves a list of files at the top-level of the given repository from the given API.
	async fn get_file_tree(
		&self,
		net: &Net,
		username: &PercentEncoded,
		repo_name: &PercentEncoded,
	) -> Result<Vec<responses::Item>, error::Error>;

	/// Retrieves the 12 most recent repositories.
	async fn get_recent_repos(&self, net: &Net) -> Result<Vec<responses::Repo>, error::Error>;

	/// Retrieves repository metadata.
	async fn get_repo(
		&self,
		net: &Net,
		username: &PercentEncoded,
		repo_name: &PercentEncoded,
	) -> Result<responses::Repo, error::Error>;

	/// Retrieves the user's repositories.
	async fn get_user_repos(
		&self,
		net: &Net,
		username: &PercentEncoded,
	) -> Result<Vec<responses::Repo>, error::Error>;

	/// Retrieves the users.
	async fn get_users(&self, net: &Net) -> Result<Vec<responses::User>, error::Error>;

	/// Retrieves the forge version.
	async fn get_version(&self, net: &Net) -> Result<responses::ServerVersion, error::Error>;

	/// Makes an authenticated API request to determine whether our access token is valid.
	/// Returns [`Ok`] if the token is valid or was not provided (we assume it isn't needed),
	/// and [`Err`] with [`error::Error::Unauthorized`] if not. Other relevant errors may be
	/// returned as well, such as [`error::Error::NetworkFailure`] or [`error::Error::Restricted`].
	async fn check_access_token(&self, net: &Net) -> Result<(), error::Error>;

	/// The header name to use for authenticating requests.
	fn access_token_header_name(&self) -> HeaderName {
		AUTHORIZATION
	}

	/// The access token with which to authenticate requests. If [`None`]
	/// is returned, then no additional auth header will be sent.
	fn access_token_value(&self) -> Option<SecretHeader> {
		None
	}
}

pub enum Api {
	Forgejo(ForgejoApi),
	Gitea(ForgejoApi), // Forgejo == Gitea for now, as far as our use of the APIs is concerned
	GitLab(GitLabApi),
}

#[async_trait]
impl ForgeApi for Api {
	fn kind(&self) -> ForgeApiKind {
		match self {
			Self::Forgejo(_) => ForgeApiKind::Forgejo,
			Self::GitLab(_) => ForgeApiKind::GitLab,
			Self::Gitea(_) => ForgeApiKind::Gitea,
		}
	}

	fn base_url(&self) -> &Url {
		match self {
			Self::Forgejo(api) => api.base_url(),
			Self::GitLab(api) => api.base_url(),
			Self::Gitea(api) => api.base_url(),
		}
	}

	async fn get_branches(
		&self,
		net: &Net,
		username: &PercentEncoded,
		repo_name: &PercentEncoded,
	) -> Result<Vec<responses::Branch>, error::Error> {
		match self {
			Self::Forgejo(api) => api.get_branches(net, username, repo_name).await,
			Self::GitLab(api) => api.get_branches(net, username, repo_name).await,
			Self::Gitea(api) => api.get_branches(net, username, repo_name).await,
		}
	}

	async fn get_contents(
		&self,
		net: &Net,
		username: &PercentEncoded,
		repo_name: &PercentEncoded,
		branch_name: &PercentEncoded,
		item_path: &PercentEncoded,
	) -> Result<responses::Contents<responses::Item>, error::Error> {
		match self {
			Self::Forgejo(api) => {
				api.get_contents(net, username, repo_name, branch_name, item_path)
					.await
			}
			Self::GitLab(api) => {
				api.get_contents(net, username, repo_name, branch_name, item_path)
					.await
			}
			Self::Gitea(api) => {
				api.get_contents(net, username, repo_name, branch_name, item_path)
					.await
			}
		}
	}

	async fn get_file_tree(
		&self,
		net: &Net,
		username: &PercentEncoded,
		repo_name: &PercentEncoded,
	) -> Result<Vec<responses::Item>, error::Error> {
		match self {
			Self::Forgejo(api) => api.get_file_tree(net, username, repo_name).await,
			Self::GitLab(api) => api.get_file_tree(net, username, repo_name).await,
			Self::Gitea(api) => api.get_file_tree(net, username, repo_name).await,
		}
	}

	async fn get_recent_repos(&self, net: &Net) -> Result<Vec<responses::Repo>, error::Error> {
		match self {
			Self::Forgejo(api) => api.get_recent_repos(net).await,
			Self::GitLab(api) => api.get_recent_repos(net).await,
			Self::Gitea(api) => api.get_recent_repos(net).await,
		}
	}

	async fn get_repo(
		&self,
		net: &Net,
		username: &PercentEncoded,
		repo_name: &PercentEncoded,
	) -> Result<responses::Repo, error::Error> {
		match self {
			Self::Forgejo(api) => api.get_repo(net, username, repo_name).await,
			Self::GitLab(api) => api.get_repo(net, username, repo_name).await,
			Self::Gitea(api) => api.get_repo(net, username, repo_name).await,
		}
	}

	async fn get_user_repos(
		&self,
		net: &Net,
		username: &PercentEncoded,
	) -> Result<Vec<responses::Repo>, error::Error> {
		match self {
			Self::Forgejo(api) => api.get_user_repos(net, username).await,
			Self::GitLab(api) => api.get_user_repos(net, username).await,
			Self::Gitea(api) => api.get_user_repos(net, username).await,
		}
	}

	async fn get_users(&self, net: &Net) -> Result<Vec<responses::User>, error::Error> {
		match self {
			Self::Forgejo(api) => api.get_users(net).await,
			Self::GitLab(api) => api.get_users(net).await,
			Self::Gitea(api) => api.get_users(net).await,
		}
	}

	async fn get_version(&self, net: &Net) -> Result<responses::ServerVersion, error::Error> {
		match self {
			Self::Forgejo(api) => api.get_version(net).await,
			Self::GitLab(api) => api.get_version(net).await,
			Self::Gitea(api) => api.get_version(net).await,
		}
	}

	async fn check_access_token(&self, net: &Net) -> Result<(), error::Error> {
		match self {
			Self::Forgejo(api) => api.check_access_token(net).await,
			Self::GitLab(api) => api.check_access_token(net).await,
			Self::Gitea(api) => api.check_access_token(net).await,
		}
	}

	fn access_token_header_name(&self) -> HeaderName {
		match self {
			Self::Forgejo(api) => api.access_token_header_name(),
			Self::GitLab(api) => api.access_token_header_name(),
			Self::Gitea(api) => api.access_token_header_name(),
		}
	}

	fn access_token_value(&self) -> Option<SecretHeader> {
		match self {
			Self::Forgejo(api) => api.access_token_value(),
			Self::GitLab(api) => api.access_token_value(),
			Self::Gitea(api) => api.access_token_value(),
		}
	}
}