use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; use axum::extract::State; use axum::response::{IntoResponse, Json}; use axum::routing::post; use axum::Router; use hyper::header::CONTENT_TYPE; use hyper::Method; use serde::{Deserialize, Serialize}; use tower_http::cors::{self, CorsLayer}; use radicle::{git, identity}; use radicle_types as types; use radicle_types::cobs::issue; use radicle_types::cobs::issue::NewIssue; use radicle_types::cobs::CobOptions; use radicle_types::cobs::{self, FromRadicleAction}; use radicle_types::config::Version; use radicle_types::domain::patch::models; use radicle_types::domain::patch::service::Service; use radicle_types::domain::patch::traits::PatchService; use radicle_types::error::Error; use radicle_types::outbound::sqlite::Sqlite; use radicle_types::traits::cobs::Cobs; use radicle_types::traits::issue::{Issues, IssuesMut}; use radicle_types::traits::job::Jobs; use radicle_types::traits::patch::{Patches, PatchesMut}; use radicle_types::traits::repo::{Repo, Show}; use radicle_types::traits::thread::Thread; use radicle_types::traits::Profile; #[derive(Clone)] pub struct Context { profile: Arc, patches: Arc>, } impl Repo for Context {} impl Cobs for Context {} impl Thread for Context {} impl Issues for Context {} impl IssuesMut for Context {} impl Jobs for Context {} impl Patches for Context {} impl PatchesMut for Context {} impl Profile for Context { fn profile(&self) -> radicle::Profile { self.profile.deref().clone() } } impl Context { pub fn new(profile: Arc, patches: Arc>) -> Self { Self { profile, patches } } } pub fn router(ctx: Context) -> Router { Router::new() .route("/config", post(config_handler)) .route("/authenticate", post(auth_handler)) .route("/repo_count", post(repo_count_handler)) .route("/list_repos", post(repo_root_handler)) .route("/list_repos_summary", post(list_repos_summary_handler)) .route( "/seeded_not_replicated", post(seeded_not_replicated_handler), ) .route("/repo_by_id", post(repo_handler)) .route("/version", post(version_handler)) .route("/diff_stats", post(diff_stats_handler)) .route( "/activity_by_issue", post(activity_issue_handler::), ) .route( "/activity_by_patch", post(activity_patch_handler::), ) .route("/repo_readme", post(readme_handler)) .route("/repo_tree", post(tree_handler)) .route("/repo_blob", post(blob_handler)) .route("/get_diff", post(diff_handler)) .route("/get_commit_diff", post(commit_diff_handler)) .route("/list_repo_commits", post(list_repo_commits_handler)) .route("/repo_commit_count", post(repo_commit_count_handler)) .route("/repo_commit", post(repo_commit_handler)) .route("/list_issues", post(issues_handler)) .route("/create_issue", post(create_issue_handler)) .route("/create_issue_comment", post(create_issue_comment_handler)) .route("/edit_issue", post(edit_issue_handler)) .route("/issue_by_id", post(issue_handler)) .route("/comment_threads_by_issue_id", post(issue_threads_handler)) .route("/list_patches", post(patches_handler)) .route("/patch_by_id", post(patch_handler)) .route("/revisions_by_patch", post(revision_handler)) .route("/get_embed", post(get_embeds_handler)) .route("/save_embed_by_path", post(save_embed_handler)) .route("/save_embed_by_clipboard", post(save_embed_handler)) .route("/save_embed_by_bytes", post(save_embed_handler)) .route("/save_embed_to_disk", post(save_embed_handler)) .route("/list_jobs", post(jobs_handler)) .route("/list_notifications", post(list_notifications_handler)) .route("/notification_count", post(notification_count_handler)) .route("/clear_notifications", post(clear_notifications_handler)) .layer( CorsLayer::new() .allow_origin(cors::Any) .allow_methods([Method::POST, Method::GET]) .allow_headers([CONTENT_TYPE]), ) .with_state(ctx) } async fn config_handler(State(ctx): State) -> impl IntoResponse { let config = ctx.config(); Ok::<_, Error>(Json(config)) } async fn auth_handler() -> impl IntoResponse { Ok::<_, Error>(Json(())) } #[derive(Serialize, Deserialize)] pub struct Options { show: Show, } async fn repo_root_handler( State(ctx): State, Json(Options { show }): Json, ) -> impl IntoResponse { let repos = ctx.list_repos(show)?; Ok::<_, Error>(Json(repos)) } async fn repo_count_handler(State(ctx): State) -> impl IntoResponse { let repos = ctx.repo_count()?; Ok::<_, Error>(Json(repos)) } async fn list_repos_summary_handler(State(ctx): State) -> impl IntoResponse { let repos = ctx.list_repos_summary()?; Ok::<_, Error>(Json(repos)) } async fn seeded_not_replicated_handler(State(ctx): State) -> impl IntoResponse { let rids = ctx.seeded_not_replicated()?; Ok::<_, Error>(Json(rids)) } async fn list_notifications_handler() -> impl IntoResponse { Ok::<_, Error>(Json(Vec::< radicle_types::domain::inbox::models::notification::NotificationsByRepo, >::new())) } async fn notification_count_handler() -> impl IntoResponse { Ok::<_, Error>(Json(0_usize)) } async fn clear_notifications_handler() -> impl IntoResponse { Ok::<_, Error>(Json(())) } #[derive(Serialize, Deserialize)] struct RepoBody { pub rid: identity::RepoId, } async fn repo_handler( State(ctx): State, Json(RepoBody { rid }): Json, ) -> impl IntoResponse { let info = ctx.repo_by_id(rid)?; Ok::<_, Error>(Json(info)) } async fn version_handler() -> impl IntoResponse { let version = Version { version: String::from("0.6.1"), head: String::from("51cf6cfbfe0be992ee709c49e6da589aa0f148c5"), }; Ok::<_, Error>(Json(version)) } #[derive(Serialize, Deserialize)] struct DiffStatsBody { pub rid: identity::RepoId, pub base: git::Oid, pub head: git::Oid, } async fn diff_stats_handler( State(ctx): State, Json(DiffStatsBody { rid, base, head }): Json, ) -> impl IntoResponse { let info = ctx.diff_stats(rid, base, head)?; Ok::<_, Error>(Json(info)) } #[derive(Serialize, Deserialize)] struct DiffBody { pub rid: identity::RepoId, pub options: types::cobs::diff::DiffOptions, } #[derive(Serialize, Deserialize)] struct ReadmeBody { pub rid: identity::RepoId, } async fn readme_handler( State(ctx): State, Json(ReadmeBody { rid }): Json, ) -> impl IntoResponse { let readme = ctx.repo_readme(rid, None)?; Ok::<_, Error>(Json(readme)) } #[derive(Serialize, Deserialize)] struct TreeBody { pub rid: identity::RepoId, pub path: PathBuf, } async fn tree_handler( State(ctx): State, Json(TreeBody { rid, path }): Json, ) -> impl IntoResponse { let info = ctx.repo_tree(rid, path)?; Ok::<_, Error>(Json(info)) } #[derive(Serialize, Deserialize)] struct BlobBody { pub rid: identity::RepoId, pub path: PathBuf, } async fn blob_handler( State(ctx): State, Json(BlobBody { rid, path }): Json, ) -> impl IntoResponse { let info = ctx.repo_blob(rid, path)?; Ok::<_, Error>(Json(info)) } async fn diff_handler( State(ctx): State, Json(DiffBody { rid, options }): Json, ) -> impl IntoResponse { let info = ctx.get_diff(rid, options)?; Ok::<_, Error>(Json(info)) } #[derive(Serialize, Deserialize)] struct CommitDiffBody { pub rid: identity::RepoId, pub sha: git::Oid, pub unified: Option, pub highlight: Option, } async fn commit_diff_handler( State(ctx): State, Json(CommitDiffBody { rid, sha, unified, highlight, }): Json, ) -> impl IntoResponse { let diff = ctx.get_commit_diff(rid, sha, unified, highlight)?; Ok::<_, Error>(Json(diff)) } #[derive(Serialize, Deserialize)] struct ListRepoCommitsBody { pub rid: identity::RepoId, pub head: Option, pub skip: Option, pub take: Option, } async fn list_repo_commits_handler( State(ctx): State, Json(ListRepoCommitsBody { rid, head, skip, take, }): Json, ) -> impl IntoResponse { let commits = ctx.list_repo_commits(rid, head, skip, take)?; Ok::<_, Error>(Json(commits)) } #[derive(Serialize, Deserialize)] struct RepoCommitCountBody { pub rid: identity::RepoId, pub head: git::Oid, } async fn repo_commit_count_handler( State(ctx): State, Json(RepoCommitCountBody { rid, head }): Json, ) -> impl IntoResponse { let count = ctx.repo_commit_count(rid, head)?; Ok::<_, Error>(Json(count)) } #[derive(Serialize, Deserialize)] struct RepoCommitBody { pub rid: identity::RepoId, pub sha: git::Oid, } async fn repo_commit_handler( State(ctx): State, Json(RepoCommitBody { rid, sha }): Json, ) -> impl IntoResponse { let commit = ctx.repo_commit(rid, sha)?; Ok::<_, Error>(Json(commit)) } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct IssuesBody { pub rid: identity::RepoId, pub status: Option, } async fn issues_handler( State(ctx): State, Json(IssuesBody { rid, status }): Json, ) -> impl IntoResponse { let issues = ctx.list_issues(rid, status)?; Ok::<_, Error>(Json(issues)) } #[derive(Serialize, Deserialize)] struct CreateIssuesBody { pub rid: identity::RepoId, pub opts: CobOptions, pub new: NewIssue, } async fn create_issue_handler( State(ctx): State, Json(CreateIssuesBody { rid, opts, new }): Json, ) -> impl IntoResponse { let issues = ctx.create_issue(rid, new, opts)?; Ok::<_, Error>(Json(issues)) } #[derive(Serialize, Deserialize)] struct CreateIssueCommentBody { pub rid: identity::RepoId, pub new: types::cobs::thread::NewIssueComment, pub opts: types::cobs::CobOptions, } async fn create_issue_comment_handler( State(ctx): State, Json(CreateIssueCommentBody { rid, opts, new }): Json, ) -> impl IntoResponse { let comment = ctx.create_issue_comment(rid, new, opts)?; Ok::<_, Error>(Json(comment)) } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct EditIssuesBody { pub rid: identity::RepoId, pub cob_id: git::Oid, pub action: issue::Action, pub opts: CobOptions, } async fn edit_issue_handler( State(ctx): State, Json(EditIssuesBody { rid, cob_id, action, opts, }): Json, ) -> impl IntoResponse { let issues = ctx.edit_issue(rid, cob_id, action, opts)?; Ok::<_, Error>(Json(issues)) } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct ActivityBody { pub rid: identity::RepoId, pub id: git::Oid, } async fn activity_issue_handler< A: serde::Serialize + serde::de::DeserializeOwned, B: FromRadicleAction + serde::Serialize, >( State(ctx): State, Json(ActivityBody { rid, id }): Json, ) -> impl IntoResponse { let activity = ctx.activity_by_id::(rid, &radicle::cob::issue::TYPENAME, id)?; Ok::<_, Error>(Json(activity)) } async fn activity_patch_handler< A: serde::Serialize + serde::de::DeserializeOwned, B: FromRadicleAction + serde::Serialize, >( State(ctx): State, Json(ActivityBody { rid, id }): Json, ) -> impl IntoResponse { let activity = ctx.activity_by_id::(rid, &radicle::cob::patch::TYPENAME, id)?; Ok::<_, Error>(Json(activity)) } #[derive(Serialize, Deserialize)] struct EmbedBody { pub rid: identity::RepoId, pub name: Option, pub oid: git::Oid, } async fn get_embeds_handler( State(ctx): State, Json(EmbedBody { rid, name, oid }): Json, ) -> impl IntoResponse { let embed = ctx.get_embed(rid, name, oid)?; Ok::<_, Error>(Json(embed)) } #[derive(Serialize, Deserialize)] struct CreateEmbedBody { pub rid: identity::RepoId, pub path: PathBuf, } async fn save_embed_handler( State(ctx): State, Json(CreateEmbedBody { rid, path }): Json, ) -> impl IntoResponse { let embed = ctx.save_embed_by_path(rid, path)?; Ok::<_, Error>(Json(embed)) } #[derive(Serialize, Deserialize)] struct IssueBody { pub rid: identity::RepoId, pub id: git::Oid, } async fn issue_handler( State(ctx): State, Json(IssueBody { rid, id }): Json, ) -> impl IntoResponse { let issue = ctx.issue_by_id(rid, id)?; Ok::<_, Error>(Json(issue)) } async fn issue_threads_handler( State(ctx): State, Json(IssueBody { rid, id }): Json, ) -> impl IntoResponse { let issue_threads = ctx.comment_threads_by_issue_id(rid, id)?; Ok::<_, Error>(Json(issue_threads)) } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct PatchesBody { pub rid: identity::RepoId, pub skip: Option, pub take: Option, pub status: Option, } async fn patches_handler( State(ctx): State, Json(PatchesBody { rid, skip, take, status, }): Json, ) -> impl IntoResponse { let profile = ctx.profile; let cursor = skip.unwrap_or(0); let aliases = profile.aliases(); let patches = match status { None => ctx.patches.list(rid)?.collect::>(), Some(s) => ctx .patches .list_by_status(rid, s.into())? .collect::>(), }; if let Some(t) = take { if t < 0 { // Return all patches let content = patches .into_iter() .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases)) .collect::>(); return Ok::<_, Error>(Json(cobs::PaginatedQuery { cursor: 0, more: false, content, })); } } let take = take.unwrap_or(20) as usize; let more = cursor + take < patches.len(); let patches = patches .into_iter() .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases)) .skip(cursor) .take(take) .collect::>(); Ok::<_, Error>(Json(cobs::PaginatedQuery { cursor, more, content: patches, })) } #[derive(Serialize, Deserialize)] struct PatchBody { pub rid: identity::RepoId, pub id: git::Oid, } async fn patch_handler( State(ctx): State, Json(PatchBody { rid, id }): Json, ) -> impl IntoResponse { let patch = ctx.get_patch(rid, id)?; Ok::<_, Error>(Json(patch)) } async fn revision_handler( State(ctx): State, Json(PatchBody { rid, id }): Json, ) -> impl IntoResponse { let revisions = ctx.revisions_by_patch(rid, id)?; Ok::<_, Error>(Json(revisions)) } #[derive(Serialize, Deserialize)] struct JobsBody { pub rid: identity::RepoId, pub sha: git::Oid, } async fn jobs_handler( State(ctx): State, Json(JobsBody { rid, sha }): Json, ) -> impl IntoResponse { let jobs = ctx.list_jobs(rid, sha)?; Ok::<_, Error>(Json(jobs)) }