mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-27 23:50:36 +08:00
Move command logic from Tauri handlers into radicle-types
Auth, inbox, CLI detection and cache rebuilds lived only in the Tauri commands, so test-http-api couldn't share them and stubbed them instead.
This commit is contained in:
@@ -1,11 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use radicle::crypto::Seed;
|
||||
use radicle::crypto::ssh::{self, Passphrase};
|
||||
use radicle::node::Alias;
|
||||
use radicle::profile::env;
|
||||
use radicle::crypto::ssh::Passphrase;
|
||||
use radicle_types::error::Error;
|
||||
use ssh_key::rand_core::{OsRng, RngCore};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
@@ -14,78 +8,10 @@ pub fn authenticate(
|
||||
ctx: tauri::State<AppState>,
|
||||
passphrase: Option<Passphrase>,
|
||||
) -> Result<(), Error> {
|
||||
let profile = &ctx.profile;
|
||||
if !profile.keystore.is_encrypted()? {
|
||||
return Ok(());
|
||||
}
|
||||
match ssh::agent::Agent::connect() {
|
||||
Ok(mut agent) => {
|
||||
if agent.request_identities()?.contains(&profile.public_key) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match passphrase {
|
||||
Some(passphrase) => {
|
||||
profile.keystore.secret_key(Some(passphrase.clone()))?;
|
||||
register(&mut agent, profile, passphrase)
|
||||
}
|
||||
None => Err(Error::Crypto(
|
||||
radicle::crypto::ssh::keystore::Error::PassphraseMissing,
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(e) if e.is_not_running() => Err(Error::AgentNotRunning)?,
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
radicle_types::auth::authenticate(&ctx.profile, passphrase)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn init(alias: String, passphrase: Passphrase) -> Result<(), Error> {
|
||||
let home = radicle::profile::home()?;
|
||||
let alias = Alias::from_str(&alias)?;
|
||||
|
||||
if passphrase.is_empty() {
|
||||
return Err(Error::Crypto(
|
||||
radicle::crypto::ssh::keystore::Error::PassphraseMissing,
|
||||
));
|
||||
}
|
||||
let seed = env::seed().unwrap_or_else(|| {
|
||||
let mut bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut bytes);
|
||||
Seed::new(bytes)
|
||||
});
|
||||
let profile = radicle::Profile::init(home, alias, Some(passphrase.clone()), seed)?;
|
||||
match ssh::agent::Agent::connect() {
|
||||
Ok(mut agent) => register(&mut agent, &profile, passphrase.clone())?,
|
||||
Err(e) if e.is_not_running() => return Err(Error::AgentNotRunning),
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
agent: &mut ssh::agent::Agent,
|
||||
profile: &radicle::Profile,
|
||||
passphrase: ssh::Passphrase,
|
||||
) -> Result<(), Error> {
|
||||
let secret = profile
|
||||
.keystore
|
||||
.secret_key(Some(passphrase))
|
||||
.map_err(|e| {
|
||||
if e.is_crypto_err() {
|
||||
Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh(
|
||||
ssh_key::Error::Crypto,
|
||||
))
|
||||
} else {
|
||||
e.into()
|
||||
}
|
||||
})?
|
||||
.ok_or(Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh(
|
||||
ssh_key::Error::Crypto,
|
||||
)))?;
|
||||
|
||||
agent.register(&secret)?;
|
||||
|
||||
Ok(())
|
||||
radicle_types::auth::init(alias, passphrase)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use radicle::git;
|
||||
use radicle::identity;
|
||||
use radicle::storage::ReadStorage;
|
||||
|
||||
use radicle::issue::TYPENAME;
|
||||
use radicle_types as types;
|
||||
@@ -97,30 +94,5 @@ pub async fn rebuild_issue_cache(
|
||||
rid: identity::RepoId,
|
||||
on_event: tauri::ipc::Channel<types::cobs::CacheEvent>,
|
||||
) -> Result<(), Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let signer = ctx.profile.signer()?;
|
||||
let mut issues = ctx.profile.issues_mut(&repo, &signer)?;
|
||||
on_event.send(types::cobs::CacheEvent::Started { rid })?;
|
||||
issues.write_all(|result, progress| {
|
||||
match result {
|
||||
Ok((id, _)) => {
|
||||
if on_event
|
||||
.send(types::cobs::CacheEvent::Progress {
|
||||
rid,
|
||||
oid: **id,
|
||||
current: progress.current(),
|
||||
total: progress.total(),
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to send progress");
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("Failed to retrieve issue: {err}"),
|
||||
};
|
||||
ControlFlow::Continue(())
|
||||
})?;
|
||||
on_event.send(types::cobs::CacheEvent::Finished { rid })?;
|
||||
|
||||
Ok(())
|
||||
ctx.rebuild_issue_cache(rid, |event| on_event.send(event).map_err(Error::from))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use radicle::patch::{ReviewId, TYPENAME};
|
||||
use radicle::storage::{ReadRepository as _, ReadStorage};
|
||||
use radicle::{git, identity};
|
||||
@@ -97,30 +95,5 @@ pub async fn rebuild_patch_cache(
|
||||
rid: identity::RepoId,
|
||||
on_event: tauri::ipc::Channel<cobs::CacheEvent>,
|
||||
) -> Result<(), Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let signer = ctx.profile.signer()?;
|
||||
let mut patches = ctx.profile.patches_mut(&repo, &signer)?;
|
||||
on_event.send(types::cobs::CacheEvent::Started { rid })?;
|
||||
patches.write_all(|result, progress| {
|
||||
match result {
|
||||
Ok((id, _)) => {
|
||||
if on_event
|
||||
.send(cobs::CacheEvent::Progress {
|
||||
rid,
|
||||
oid: **id,
|
||||
current: progress.current(),
|
||||
total: progress.total(),
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to send progress");
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("Failed to retrieve patch: {err}"),
|
||||
};
|
||||
ControlFlow::Continue(())
|
||||
})?;
|
||||
on_event.send(types::cobs::CacheEvent::Finished { rid })?;
|
||||
|
||||
Ok(())
|
||||
ctx.rebuild_patch_cache(rid, |event| on_event.send(event).map_err(Error::from))
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
use radicle::identity;
|
||||
use radicle::issue::cache::Issues;
|
||||
use radicle::node;
|
||||
use radicle::patch::cache::Patches;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
|
||||
use radicle_types::AppState;
|
||||
use radicle_types::domain::inbox::models::notification::{self, RepoGroupByItem};
|
||||
use radicle_types::domain::inbox::models::notification;
|
||||
use radicle_types::domain::inbox::service::Service;
|
||||
use radicle_types::domain::inbox::traits::InboxService;
|
||||
use radicle_types::error::Error;
|
||||
use radicle_types::outbound::sqlite::Sqlite;
|
||||
use radicle_types::traits::inbox::Inbox;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_notifications(
|
||||
@@ -17,177 +11,15 @@ pub fn list_notifications(
|
||||
sqlite_service: tauri::State<Service<Sqlite>>,
|
||||
params: notification::RepoGroupParams,
|
||||
) -> Result<notification::NotificationsByRepoList, Error> {
|
||||
let profile = &ctx.profile;
|
||||
let aliases = profile.aliases();
|
||||
let repos_with_groups = sqlite_service.repo_group(params.clone())?;
|
||||
|
||||
let mut repo_counts = std::collections::HashMap::new();
|
||||
for (repo_id, count) in (sqlite_service.counts_by_repo()?).flatten() {
|
||||
repo_counts.insert(repo_id, count);
|
||||
}
|
||||
|
||||
let take = if params.all.unwrap_or(false) {
|
||||
usize::MAX
|
||||
} else {
|
||||
params.take.unwrap_or(20)
|
||||
};
|
||||
|
||||
let mut grouped_repos = std::collections::HashMap::new();
|
||||
for (repo_id, group) in repos_with_groups {
|
||||
grouped_repos
|
||||
.entry(repo_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.extend(group);
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for (repo_id, all) in grouped_repos {
|
||||
let repo = match profile.storage.repository(repo_id) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log::error!("Failed to open repository {}: {}", repo_id, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let name = match repo.identity_doc() {
|
||||
Ok(identity::DocAt { doc, .. }) => match doc.project() {
|
||||
Ok(project) => project.name().to_string(),
|
||||
Err(_) => format!("Unknown project in {}", repo_id),
|
||||
},
|
||||
Err(_) => format!("Unknown project in {}", repo_id),
|
||||
};
|
||||
|
||||
let patches = match profile.patches(&repo) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::error!("Failed to get patches for {}: {}", repo_id, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let issues = match profile.issues(&repo) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
log::error!("Failed to get issues for {}: {}", repo_id, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let content = all
|
||||
.into_iter()
|
||||
.take(take)
|
||||
.map(|(qualified, n)| {
|
||||
n.into_iter()
|
||||
.filter_map(|s| {
|
||||
let update: notification::RefUpdate =
|
||||
(qualified.clone().into_refstring(), s.new, s.old).into();
|
||||
let update: radicle::storage::RefUpdate = update.into();
|
||||
let kind =
|
||||
node::notifications::NotificationKind::try_from(qualified.clone())
|
||||
.ok()?;
|
||||
|
||||
match kind {
|
||||
node::notifications::NotificationKind::Cob { ref typed_id } => {
|
||||
if typed_id.is_patch() {
|
||||
let actions = notification::actions(
|
||||
typed_id.type_name.clone(),
|
||||
typed_id.id,
|
||||
update.old(),
|
||||
update.new(),
|
||||
&repo,
|
||||
&aliases,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
match patches.get(&typed_id.id) {
|
||||
Ok(Some(p)) => Some(notification::NotificationItem::Patch(
|
||||
notification::Patch {
|
||||
row_id: s.row_id,
|
||||
id: typed_id.id,
|
||||
update: update.into(),
|
||||
timestamp: s.timestamp,
|
||||
title: p.title().to_string(),
|
||||
status: (p.state().clone()).into(),
|
||||
actions,
|
||||
repo_id: Some(repo_id),
|
||||
},
|
||||
)),
|
||||
Ok(None) => {
|
||||
log::error!("No patch found");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if typed_id.is_issue() {
|
||||
let actions = notification::actions(
|
||||
typed_id.type_name.clone(),
|
||||
typed_id.id,
|
||||
update.old(),
|
||||
update.new(),
|
||||
&repo,
|
||||
&aliases,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
match issues.get(&typed_id.id) {
|
||||
Ok(Some(i)) => Some(notification::NotificationItem::Issue(
|
||||
notification::Issue {
|
||||
row_id: s.row_id,
|
||||
id: typed_id.id,
|
||||
update: update.into(),
|
||||
timestamp: s.timestamp,
|
||||
title: i.title().to_string(),
|
||||
status: (*i.state()).into(),
|
||||
actions,
|
||||
repo_id: Some(repo_id),
|
||||
},
|
||||
)),
|
||||
Ok(None) => {
|
||||
log::error!("No issue found");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|v| !v.is_empty())
|
||||
.collect::<RepoGroupByItem>();
|
||||
|
||||
if !content.is_empty() {
|
||||
let count = repo_counts
|
||||
.get(&repo_id)
|
||||
.copied()
|
||||
.unwrap_or_else(|| content.iter().map(|items| items.len()).sum());
|
||||
|
||||
result.push(notification::NotificationsByRepo {
|
||||
rid: repo_id,
|
||||
name,
|
||||
notifications: content,
|
||||
count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
ctx.list_notifications(&sqlite_service, params)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn notification_count(inbox: tauri::State<Service<Sqlite>>) -> Result<usize, Error> {
|
||||
inbox.notification_count().map_err(Error::from)
|
||||
pub fn notification_count(
|
||||
ctx: tauri::State<AppState>,
|
||||
inbox: tauri::State<Service<Sqlite>>,
|
||||
) -> Result<usize, Error> {
|
||||
ctx.notification_count(&inbox)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -195,13 +27,5 @@ pub fn clear_notifications(
|
||||
ctx: tauri::State<AppState>,
|
||||
params: notification::SetStatusNotifications,
|
||||
) -> Result<(), Error> {
|
||||
let profile = &ctx.profile;
|
||||
let mut notifications = profile.notifications_mut()?;
|
||||
match params {
|
||||
notification::SetStatusNotifications::Ids(ids) => notifications.clear(&ids)?,
|
||||
notification::SetStatusNotifications::Repo(repo) => notifications.clear_by_repo(&repo)?,
|
||||
notification::SetStatusNotifications::All => notifications.clear_all()?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
ctx.clear_notifications(params)
|
||||
}
|
||||
|
||||
@@ -19,17 +19,7 @@ pub(crate) fn version(app: AppHandle) -> Result<Version, Error> {
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn check_radicle_cli(ctx: tauri::State<AppState>) -> Result<(), Error> {
|
||||
// Where the official installer puts it, even if it never reached `PATH`.
|
||||
let rad = ctx.profile().home().path().join("bin").join("rad");
|
||||
if rad.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if radicle_types::binaries::rad().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(Error::RadicleNotInstalled)
|
||||
radicle_types::binaries::check_radicle_cli(&ctx.profile())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -40,10 +30,9 @@ pub(crate) fn git_info() -> GitInfo {
|
||||
#[tauri::command]
|
||||
pub(crate) fn startup(app: AppHandle) -> Result<Config, Error> {
|
||||
let profile = radicle::Profile::load()?;
|
||||
let home = profile.home();
|
||||
|
||||
let cobs_cache = radicle::cob::cache::Store::open(home.cobs().join(COBS_DB_FILE))?;
|
||||
cobs_cache.check_version()?;
|
||||
let state = AppState { profile };
|
||||
state.check_cobs_cache()?;
|
||||
let profile = &state.profile;
|
||||
|
||||
let inbox_db = radicle_types::outbound::sqlite::Sqlite::reader(
|
||||
profile.node().join(NOTIFICATIONS_DB_FILE),
|
||||
@@ -70,7 +59,6 @@ pub(crate) fn startup(app: AppHandle) -> Result<Config, Error> {
|
||||
}
|
||||
});
|
||||
|
||||
let state = AppState { profile };
|
||||
app.manage(state.clone());
|
||||
|
||||
Ok(state.config())
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::error::Error;
|
||||
use radicle::crypto::Seed;
|
||||
use radicle::crypto::ssh::{self, Passphrase};
|
||||
use radicle::node::Alias;
|
||||
use radicle::profile::env;
|
||||
use ssh_key::rand_core::{OsRng, RngCore};
|
||||
|
||||
pub fn authenticate(
|
||||
profile: &radicle::Profile,
|
||||
passphrase: Option<Passphrase>,
|
||||
) -> Result<(), Error> {
|
||||
if !profile.keystore.is_encrypted()? {
|
||||
return Ok(());
|
||||
}
|
||||
match ssh::agent::Agent::connect() {
|
||||
Ok(mut agent) => {
|
||||
if agent.request_identities()?.contains(&profile.public_key) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match passphrase {
|
||||
Some(passphrase) => {
|
||||
profile.keystore.secret_key(Some(passphrase.clone()))?;
|
||||
register(&mut agent, profile, passphrase)
|
||||
}
|
||||
None => Err(Error::Crypto(
|
||||
radicle::crypto::ssh::keystore::Error::PassphraseMissing,
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(e) if e.is_not_running() => Err(Error::AgentNotRunning)?,
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(alias: String, passphrase: Passphrase) -> Result<(), Error> {
|
||||
let home = radicle::profile::home()?;
|
||||
let alias = Alias::from_str(&alias)?;
|
||||
|
||||
if passphrase.is_empty() {
|
||||
return Err(Error::Crypto(
|
||||
radicle::crypto::ssh::keystore::Error::PassphraseMissing,
|
||||
));
|
||||
}
|
||||
let seed = env::seed().unwrap_or_else(|| {
|
||||
let mut bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut bytes);
|
||||
Seed::new(bytes)
|
||||
});
|
||||
let profile = radicle::Profile::init(home, alias, Some(passphrase.clone()), seed)?;
|
||||
match ssh::agent::Agent::connect() {
|
||||
Ok(mut agent) => register(&mut agent, &profile, passphrase.clone())?,
|
||||
Err(e) if e.is_not_running() => return Err(Error::AgentNotRunning),
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
agent: &mut ssh::agent::Agent,
|
||||
profile: &radicle::Profile,
|
||||
passphrase: ssh::Passphrase,
|
||||
) -> Result<(), Error> {
|
||||
let secret = profile
|
||||
.keystore
|
||||
.secret_key(Some(passphrase))
|
||||
.map_err(|e| {
|
||||
if e.is_crypto_err() {
|
||||
Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh(
|
||||
ssh_key::Error::Crypto,
|
||||
))
|
||||
} else {
|
||||
e.into()
|
||||
}
|
||||
})?
|
||||
.ok_or(Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh(
|
||||
ssh_key::Error::Crypto,
|
||||
)))?;
|
||||
|
||||
agent.register(&secret)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -104,6 +104,14 @@ pub fn git_command() -> Option<std::process::Command> {
|
||||
Some(command)
|
||||
}
|
||||
|
||||
pub fn check_radicle_cli(profile: &radicle::Profile) -> Result<(), crate::error::Error> {
|
||||
if profile.home().path().join("bin").join("rad").exists() || rad().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(crate::error::Error::RadicleNotInstalled)
|
||||
}
|
||||
|
||||
/// The `rad` CLI, if it is installed.
|
||||
///
|
||||
/// Uncached: the app polls for it so that installing the CLI while the app is
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use traits::Profile;
|
||||
use traits::cobs::Cobs;
|
||||
use traits::identity::Identity;
|
||||
use traits::inbox::Inbox;
|
||||
use traits::issue::{Issues, IssuesMut};
|
||||
use traits::job::Jobs;
|
||||
use traits::patch::{Patches, PatchesMut};
|
||||
use traits::repo::Repo;
|
||||
use traits::thread::Thread;
|
||||
|
||||
pub mod auth;
|
||||
pub mod binaries;
|
||||
pub mod cobs;
|
||||
pub mod config;
|
||||
@@ -29,6 +31,7 @@ impl Repo for AppState {}
|
||||
impl Thread for AppState {}
|
||||
impl Cobs for AppState {}
|
||||
impl Identity for AppState {}
|
||||
impl Inbox for AppState {}
|
||||
impl Issues for AppState {}
|
||||
impl IssuesMut for AppState {}
|
||||
impl Jobs for AppState {}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use radicle::node::{AliasStore, NodeId};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::Error;
|
||||
|
||||
pub mod cobs;
|
||||
pub mod identity;
|
||||
pub mod inbox;
|
||||
pub mod issue;
|
||||
pub mod job;
|
||||
pub mod patch;
|
||||
@@ -25,6 +27,22 @@ pub trait Profile {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_cobs_cache(&self) -> Result<(), Error> {
|
||||
let p = self.profile();
|
||||
let cache = radicle::cob::cache::Store::open(
|
||||
p.home().cobs().join(radicle::cob::cache::COBS_DB_FILE),
|
||||
)?;
|
||||
cache.check_version()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn node_running(&self) -> bool {
|
||||
let p = self.profile();
|
||||
|
||||
radicle::node::Handle::is_running(&radicle::Node::new(p.home().socket_from_env()))
|
||||
}
|
||||
|
||||
fn alias(&self, nid: NodeId) -> Option<radicle::node::Alias> {
|
||||
let p = self.profile();
|
||||
let aliases = p.aliases();
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
use radicle::identity;
|
||||
use radicle::issue::cache::Issues;
|
||||
use radicle::node;
|
||||
use radicle::patch::cache::Patches;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
|
||||
use crate::domain::inbox::models::notification::{self, RepoGroupByItem};
|
||||
use crate::domain::inbox::service::Service;
|
||||
use crate::domain::inbox::traits::InboxService;
|
||||
use crate::error::Error;
|
||||
use crate::outbound::sqlite::Sqlite;
|
||||
|
||||
use crate::traits::Profile;
|
||||
|
||||
pub trait Inbox: Profile {
|
||||
fn list_notifications(
|
||||
&self,
|
||||
sqlite_service: &Service<Sqlite>,
|
||||
params: notification::RepoGroupParams,
|
||||
) -> Result<notification::NotificationsByRepoList, Error> {
|
||||
let profile = &self.profile();
|
||||
let aliases = profile.aliases();
|
||||
let repos_with_groups = sqlite_service.repo_group(params.clone())?;
|
||||
|
||||
let mut repo_counts = std::collections::HashMap::new();
|
||||
for (repo_id, count) in (sqlite_service.counts_by_repo()?).flatten() {
|
||||
repo_counts.insert(repo_id, count);
|
||||
}
|
||||
|
||||
let take = if params.all.unwrap_or(false) {
|
||||
usize::MAX
|
||||
} else {
|
||||
params.take.unwrap_or(20)
|
||||
};
|
||||
|
||||
let mut grouped_repos = std::collections::HashMap::new();
|
||||
for (repo_id, group) in repos_with_groups {
|
||||
grouped_repos
|
||||
.entry(repo_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.extend(group);
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for (repo_id, all) in grouped_repos {
|
||||
let repo = match profile.storage.repository(repo_id) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log::error!("Failed to open repository {}: {}", repo_id, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let name = match repo.identity_doc() {
|
||||
Ok(identity::DocAt { doc, .. }) => match doc.project() {
|
||||
Ok(project) => project.name().to_string(),
|
||||
Err(_) => format!("Unknown project in {}", repo_id),
|
||||
},
|
||||
Err(_) => format!("Unknown project in {}", repo_id),
|
||||
};
|
||||
|
||||
let patches = match profile.patches(&repo) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::error!("Failed to get patches for {}: {}", repo_id, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let issues = match profile.issues(&repo) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
log::error!("Failed to get issues for {}: {}", repo_id, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let content = all
|
||||
.into_iter()
|
||||
.take(take)
|
||||
.map(|(qualified, n)| {
|
||||
n.into_iter()
|
||||
.filter_map(|s| {
|
||||
let update: notification::RefUpdate =
|
||||
(qualified.clone().into_refstring(), s.new, s.old).into();
|
||||
let update: radicle::storage::RefUpdate = update.into();
|
||||
let kind =
|
||||
node::notifications::NotificationKind::try_from(qualified.clone())
|
||||
.ok()?;
|
||||
|
||||
match kind {
|
||||
node::notifications::NotificationKind::Cob { ref typed_id } => {
|
||||
if typed_id.is_patch() {
|
||||
let actions = notification::actions(
|
||||
typed_id.type_name.clone(),
|
||||
typed_id.id,
|
||||
update.old(),
|
||||
update.new(),
|
||||
&repo,
|
||||
&aliases,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
match patches.get(&typed_id.id) {
|
||||
Ok(Some(p)) => {
|
||||
Some(notification::NotificationItem::Patch(
|
||||
notification::Patch {
|
||||
row_id: s.row_id,
|
||||
id: typed_id.id,
|
||||
update: update.into(),
|
||||
timestamp: s.timestamp,
|
||||
title: p.title().to_string(),
|
||||
status: (p.state().clone()).into(),
|
||||
actions,
|
||||
repo_id: Some(repo_id),
|
||||
},
|
||||
))
|
||||
}
|
||||
Ok(None) => {
|
||||
log::error!("No patch found");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if typed_id.is_issue() {
|
||||
let actions = notification::actions(
|
||||
typed_id.type_name.clone(),
|
||||
typed_id.id,
|
||||
update.old(),
|
||||
update.new(),
|
||||
&repo,
|
||||
&aliases,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
match issues.get(&typed_id.id) {
|
||||
Ok(Some(i)) => {
|
||||
Some(notification::NotificationItem::Issue(
|
||||
notification::Issue {
|
||||
row_id: s.row_id,
|
||||
id: typed_id.id,
|
||||
update: update.into(),
|
||||
timestamp: s.timestamp,
|
||||
title: i.title().to_string(),
|
||||
status: (*i.state()).into(),
|
||||
actions,
|
||||
repo_id: Some(repo_id),
|
||||
},
|
||||
))
|
||||
}
|
||||
Ok(None) => {
|
||||
log::error!("No issue found");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|v| !v.is_empty())
|
||||
.collect::<RepoGroupByItem>();
|
||||
|
||||
if !content.is_empty() {
|
||||
let count = repo_counts
|
||||
.get(&repo_id)
|
||||
.copied()
|
||||
.unwrap_or_else(|| content.iter().map(|items| items.len()).sum());
|
||||
|
||||
result.push(notification::NotificationsByRepo {
|
||||
rid: repo_id,
|
||||
name,
|
||||
notifications: content,
|
||||
count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn notification_count(&self, inbox: &Service<Sqlite>) -> Result<usize, Error> {
|
||||
inbox.notification_count().map_err(Error::from)
|
||||
}
|
||||
|
||||
fn clear_notifications(
|
||||
&self,
|
||||
params: notification::SetStatusNotifications,
|
||||
) -> Result<(), Error> {
|
||||
let profile = &self.profile();
|
||||
let mut notifications = profile.notifications_mut()?;
|
||||
match params {
|
||||
notification::SetStatusNotifications::Ids(ids) => notifications.clear(&ids)?,
|
||||
notification::SetStatusNotifications::Repo(repo) => {
|
||||
notifications.clear_by_repo(&repo)?
|
||||
}
|
||||
notification::SetStatusNotifications::All => notifications.clear_all()?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,39 @@ pub trait Issues: Profile {
|
||||
}
|
||||
|
||||
pub trait IssuesMut: Profile {
|
||||
fn rebuild_issue_cache(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
on_event: impl Fn(cobs::CacheEvent) -> Result<(), Error>,
|
||||
) -> Result<(), Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let signer = profile.signer()?;
|
||||
let mut issues = profile.issues_mut(&repo, &signer)?;
|
||||
on_event(cobs::CacheEvent::Started { rid })?;
|
||||
issues.write_all(|result, progress| {
|
||||
match result {
|
||||
Ok((id, _)) => {
|
||||
if on_event(cobs::CacheEvent::Progress {
|
||||
rid,
|
||||
oid: **id,
|
||||
current: progress.current(),
|
||||
total: progress.total(),
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to send progress");
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("Failed to retrieve issue: {err}"),
|
||||
};
|
||||
std::ops::ControlFlow::Continue(())
|
||||
})?;
|
||||
on_event(cobs::CacheEvent::Finished { rid })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_issue(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
|
||||
@@ -51,6 +51,39 @@ pub trait Patches: Profile {
|
||||
}
|
||||
|
||||
pub trait PatchesMut: Profile {
|
||||
fn rebuild_patch_cache(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
on_event: impl Fn(cobs::CacheEvent) -> Result<(), Error>,
|
||||
) -> Result<(), Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let signer = profile.signer()?;
|
||||
let mut patches = profile.patches_mut(&repo, &signer)?;
|
||||
on_event(cobs::CacheEvent::Started { rid })?;
|
||||
patches.write_all(|result, progress| {
|
||||
match result {
|
||||
Ok((id, _)) => {
|
||||
if on_event(cobs::CacheEvent::Progress {
|
||||
rid,
|
||||
oid: **id,
|
||||
current: progress.current(),
|
||||
total: progress.total(),
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to send progress");
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("Failed to retrieve patch: {err}"),
|
||||
};
|
||||
std::ops::ControlFlow::Continue(())
|
||||
})?;
|
||||
on_event(cobs::CacheEvent::Finished { rid })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publish a review of a revision, together with its code comments.
|
||||
fn create_patch_review(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user