feat: v0.8 Iteration A — ID-class fixes, manifest privacy, registry + greetings
A1 — ID-class bug family (v0.6.1 persona split left network-NodeId comparisons
where posting IDs live). ~14 sites fixed with multi-persona semantics ("author is
one of MY posting identities", not == default):
- revoke_post_access / revoke_circle_access (revocation was broken for every post)
- group key create/add-member/rotate (add-member distribution now works)
- circle profile set/delete/get, encrypted-attachment CEK unwrap
- receipt/comment slot authorship, replication cycle, blob eviction own-tier
- orphaned GroupKeyRequest/Response (0xA1/0xA2) deleted (no send side existed)
A2 — Manifest privacy: AuthorManifest.author_addresses removed from struct AND
signature digest; CdnManifest tree-era fields dropped. Startup migrations re-sign
own manifests, purge unverifiable foreign copies, strip persona fields from legacy
network-id profile rows. Posting identities are now location-anonymous.
A3 — Discovery & first contact:
- Open-slot comments: derivable slot V_x (blake3 from author + slot_binder_nonce)
so strangers pass the CDN comment-verification gate; OpenSlotDecl in FoF gating
- Greetings: HPKE-style sealed GreetingBody (return_path + fresh reply_pubkey),
throwaway outer ID, size-bucketed; messaging-first (Reply/Dismiss, no vouch)
- Registry: frozen canonical registry post + REGISTRY_POST_ID, signed registration
comments (self-certifying deletes), newest-wins per persona, --publish-registry
- Comment TTL: expires_at_ms inside signed digest v2 (context bump), rand 30-365d
ordinary / fixed 30d registrations, expiry sweep on the eviction cycle
- Single accept_incoming_comment() gate wired into all three ingest sites, closing
a pre-existing hole where both pull paths stored comments with no verification
- UI: consent panel (visible/listed/greetings, active choice at first publish),
registry search in Discover, sealed "Say hi" compose, greetings inbox
Security fixes from review: forged-tombstone injection, delete-by-sender-trust,
remote SetPolicy poisoning, stored XSS (escapeHtml quotes, 7 pre-existing sites),
greeting off-switch now revokes prior bios' slots.
No PoW (rejected: inverted cost). Session-relay gating untouched.
201 core tests pass; CLI + desktop build; 3-node integration green.
DEPLOY GATE: wire-incompatible with v0.7.3 — ALPN bump to itsgoin/4 lands in
Iteration B. Do not build/deploy/anchor-swap before that.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGiPD2cF75mnvneSCjdDC5
This commit is contained in:
parent
66c9851061
commit
17dbf076cb
19 changed files with 5237 additions and 524 deletions
|
|
@ -145,6 +145,8 @@ struct BadgeCountsDto {
|
|||
unread_messages: usize,
|
||||
new_reacts: usize,
|
||||
new_comments: usize,
|
||||
/// v0.8 (A3): undismissed greetings in the inbox (Messages badge adds this).
|
||||
greetings: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -279,7 +281,11 @@ async fn post_to_dto(
|
|||
// Engagement data
|
||||
let reaction_counts = {
|
||||
let storage = node.storage.get().await;
|
||||
storage.get_reaction_counts(id, &node.node_id).unwrap_or_default()
|
||||
// Reactors are posting ids — "reacted_by_me" = any of our personas.
|
||||
let our_ids: Vec<itsgoin_core::types::NodeId> = storage.list_posting_identities()
|
||||
.unwrap_or_default()
|
||||
.into_iter().map(|p| p.node_id).collect();
|
||||
storage.get_reaction_counts(id, &our_ids).unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(emoji, count, reacted_by_me)| ReactionCountDto { emoji, count, reacted_by_me })
|
||||
.collect()
|
||||
|
|
@ -555,9 +561,13 @@ async fn list_posting_identities(
|
|||
async fn create_posting_identity(
|
||||
state: State<'_, AppNode>,
|
||||
display_name: String,
|
||||
greetings_open: Option<bool>,
|
||||
) -> Result<PostingIdentityDto, String> {
|
||||
let node = get_node(&state).await;
|
||||
let id = node.create_posting_identity(display_name).await.map_err(|e| e.to_string())?;
|
||||
let id = node
|
||||
.create_posting_identity(display_name, greetings_open)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(PostingIdentityDto {
|
||||
node_id: hex::encode(id.node_id),
|
||||
display_name: id.display_name,
|
||||
|
|
@ -863,10 +873,12 @@ async fn post_to_dto_batch(
|
|||
// Batch queries — 3 queries total instead of 4 × N
|
||||
let (reaction_map, comment_map, intent_map, posting_identities) = {
|
||||
let storage = node.storage.get().await;
|
||||
let reactions = storage.get_reaction_counts_batch(&post_ids, &node.node_id).unwrap_or_default();
|
||||
let identities = storage.list_posting_identities().unwrap_or_default();
|
||||
// Reactors are posting ids — "reacted_by_me" = any of our personas.
|
||||
let our_ids: Vec<itsgoin_core::types::NodeId> = identities.iter().map(|p| p.node_id).collect();
|
||||
let reactions = storage.get_reaction_counts_batch(&post_ids, &our_ids).unwrap_or_default();
|
||||
let comments = storage.get_comment_counts_batch(&post_ids).unwrap_or_default();
|
||||
let intents = storage.get_post_intents_batch(&post_ids).unwrap_or_default();
|
||||
let identities = storage.list_posting_identities().unwrap_or_default();
|
||||
(reactions, comments, intents, identities)
|
||||
};
|
||||
// Map posting-id -> display-name so we can tag author=persona posts.
|
||||
|
|
@ -1729,6 +1741,264 @@ async fn set_session_relay_enabled(state: State<'_, AppNode>, enabled: bool) ->
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// --- v0.8 (A3): registry search + greetings ---
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RegistryEntryDto {
|
||||
author_id: String,
|
||||
display_name: String,
|
||||
keywords: Vec<String>,
|
||||
updated_at_ms: u64,
|
||||
expires_at_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RegistryStatusDto {
|
||||
listed: bool,
|
||||
keywords: Vec<String>,
|
||||
expires_at_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GreetingDto {
|
||||
/// Comment key part 1 (throwaway outer identity) — opaque to the UI,
|
||||
/// passed back verbatim for reply/dismiss.
|
||||
comment_author: String,
|
||||
/// Comment key part 2.
|
||||
post_id: String,
|
||||
/// Comment key part 3.
|
||||
timestamp_ms: u64,
|
||||
/// The sender's REAL persona (recovered from inside the seal).
|
||||
sender_persona_id: String,
|
||||
sender_name: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GreetingSlotDto {
|
||||
bio_post_id: String,
|
||||
accepts_greetings: bool,
|
||||
}
|
||||
|
||||
/// Register the persona in the network registry (30d entry, auto-renews
|
||||
/// while listed — the core eviction cycle re-signs ~every 25 days).
|
||||
#[tauri::command]
|
||||
async fn register_persona(
|
||||
state: State<'_, AppNode>,
|
||||
posting_id_hex: String,
|
||||
name: String,
|
||||
keywords: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
let node = get_node(&state).await;
|
||||
let posting_id = parse_node_id(&posting_id_hex)?;
|
||||
node.register_persona(&posting_id, &name, &keywords)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Remove the persona's registry entry (self-certifying signed delete).
|
||||
#[tauri::command]
|
||||
async fn unregister_persona(
|
||||
state: State<'_, AppNode>,
|
||||
posting_id_hex: String,
|
||||
) -> Result<(), String> {
|
||||
let node = get_node(&state).await;
|
||||
let posting_id = parse_node_id(&posting_id_hex)?;
|
||||
node.unregister_persona(&posting_id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Current "Listed" state for the consent panel: listed flag + the
|
||||
/// keywords used at last registration + entry expiry (0 if none held).
|
||||
#[tauri::command]
|
||||
async fn get_registry_status(
|
||||
state: State<'_, AppNode>,
|
||||
posting_id_hex: String,
|
||||
) -> Result<RegistryStatusDto, String> {
|
||||
let node = get_node(&state).await;
|
||||
let posting_id = parse_node_id(&posting_id_hex)?;
|
||||
let storage = node.storage.get().await;
|
||||
let listed = storage
|
||||
.get_setting(&format!("registry_listed.{}", posting_id_hex))
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false);
|
||||
let keywords = storage
|
||||
.get_setting(&format!("registry_keywords.{}", posting_id_hex))
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| {
|
||||
v.split(',')
|
||||
.map(|k| k.trim().to_string())
|
||||
.filter(|k| !k.is_empty())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let expires_at_ms = storage
|
||||
.get_newest_registry_entry(&itsgoin_core::registry::REGISTRY_POST_ID, &posting_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|(_ts, exp)| exp)
|
||||
.unwrap_or(0);
|
||||
Ok(RegistryStatusDto { listed, keywords, expires_at_ms })
|
||||
}
|
||||
|
||||
/// Search the registry: refreshes the comment chain from peers, then
|
||||
/// queries locally (search cost lands on the searcher, design §27).
|
||||
#[tauri::command]
|
||||
async fn search_registry(
|
||||
state: State<'_, AppNode>,
|
||||
query: String,
|
||||
) -> Result<Vec<RegistryEntryDto>, String> {
|
||||
let node = get_node(&state).await;
|
||||
let matches = node.search_registry(&query).await.map_err(|e| e.to_string())?;
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|m| RegistryEntryDto {
|
||||
author_id: hex::encode(m.author),
|
||||
display_name: m.name,
|
||||
keywords: m.keywords,
|
||||
updated_at_ms: m.timestamp_ms,
|
||||
expires_at_ms: m.expires_at_ms,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Locally-held greeting-slot info for an author's latest bio post.
|
||||
async fn greeting_slot_local(node: &Node, author: &NodeId) -> Option<GreetingSlotDto> {
|
||||
let storage = node.storage.get().await;
|
||||
let bio_post_id = storage
|
||||
.get_latest_profile_post_id_by_author(author)
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let post = storage.get_post(&bio_post_id).ok().flatten()?;
|
||||
let accepts = post
|
||||
.fof_gating
|
||||
.as_ref()
|
||||
.and_then(|g| g.open_slot.as_ref())
|
||||
.map(|d| d.kind == itsgoin_core::types::OpenSlotKind::Greeting)
|
||||
.unwrap_or(false);
|
||||
Some(GreetingSlotDto {
|
||||
bio_post_id: hex::encode(bio_post_id),
|
||||
accepts_greetings: accepts,
|
||||
})
|
||||
}
|
||||
|
||||
/// Does this author's bio declare a Greeting open slot? Drives the
|
||||
/// "Say hi" button in the bio modal. On-demand (A3 §3.2): if the bio
|
||||
/// isn't local yet (e.g. a registry search result), worm-locate the
|
||||
/// author and sync their posts once before answering.
|
||||
#[tauri::command]
|
||||
async fn get_greeting_slot(
|
||||
state: State<'_, AppNode>,
|
||||
node_id_hex: String,
|
||||
) -> Result<Option<GreetingSlotDto>, String> {
|
||||
let node = get_node(&state).await;
|
||||
let nid = parse_node_id(&node_id_hex)?;
|
||||
if let Some(dto) = greeting_slot_local(&node, &nid).await {
|
||||
return Ok(Some(dto));
|
||||
}
|
||||
// Fetch-on-demand: existing worm lookup + one-shot sync, then retry.
|
||||
if let Ok(Some(wr)) = node.worm_lookup(&nid).await {
|
||||
let _ = node.sync_with(wr.node_id).await;
|
||||
return Ok(greeting_slot_local(&node, &nid).await);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Send a sealed first-contact greeting to a bio post's author.
|
||||
/// Messaging-first: no vouch is involved anywhere in this flow.
|
||||
#[tauri::command]
|
||||
async fn send_greeting(
|
||||
state: State<'_, AppNode>,
|
||||
bio_post_id_hex: String,
|
||||
content: String,
|
||||
) -> Result<(), String> {
|
||||
let node = get_node(&state).await;
|
||||
let bio_post_id = hex_to_postid(&bio_post_id_hex)?;
|
||||
node.send_greeting(bio_post_id, content).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Reply to a received greeting — sealed to the greeting's fresh reply
|
||||
/// key and dropped into its declared return path (never a long-term key).
|
||||
#[tauri::command]
|
||||
async fn reply_to_greeting(
|
||||
state: State<'_, AppNode>,
|
||||
comment_author_hex: String,
|
||||
post_id_hex: String,
|
||||
timestamp_ms: u64,
|
||||
content: String,
|
||||
) -> Result<(), String> {
|
||||
let node = get_node(&state).await;
|
||||
let comment_author = parse_node_id(&comment_author_hex)?;
|
||||
let post_id = hex_to_postid(&post_id_hex)?;
|
||||
node.reply_to_greeting(comment_author, post_id, timestamp_ms, content)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Unsealed greetings (and replies) across all personas' bio posts,
|
||||
/// newest first. Return-path/reply-key internals stay inside core.
|
||||
#[tauri::command]
|
||||
async fn list_greetings(state: State<'_, AppNode>) -> Result<Vec<GreetingDto>, String> {
|
||||
let node = get_node(&state).await;
|
||||
let records = node.list_greetings().await.map_err(|e| e.to_string())?;
|
||||
Ok(records
|
||||
.into_iter()
|
||||
.map(|g| GreetingDto {
|
||||
comment_author: hex::encode(g.comment_author),
|
||||
post_id: hex::encode(g.post_id),
|
||||
timestamp_ms: g.timestamp_ms,
|
||||
sender_persona_id: hex::encode(g.sender_persona),
|
||||
sender_name: g.sender_name,
|
||||
content: g.text,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Dismiss a greeting (local only — nothing propagates).
|
||||
#[tauri::command]
|
||||
async fn dismiss_greeting(
|
||||
state: State<'_, AppNode>,
|
||||
comment_author_hex: String,
|
||||
post_id_hex: String,
|
||||
timestamp_ms: u64,
|
||||
) -> Result<(), String> {
|
||||
let node = get_node(&state).await;
|
||||
let comment_author = parse_node_id(&comment_author_hex)?;
|
||||
let post_id = hex_to_postid(&post_id_hex)?;
|
||||
node.dismiss_greeting(comment_author, post_id, timestamp_ms)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Per-persona greeting consent (republishes the bio on change).
|
||||
#[tauri::command]
|
||||
async fn set_greetings_open(
|
||||
state: State<'_, AppNode>,
|
||||
posting_id_hex: String,
|
||||
open: bool,
|
||||
) -> Result<(), String> {
|
||||
let node = get_node(&state).await;
|
||||
let posting_id = parse_node_id(&posting_id_hex)?;
|
||||
node.set_greetings_open(&posting_id, open).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Current greeting-consent state (unset = ON, the pre-checked default).
|
||||
#[tauri::command]
|
||||
async fn get_greetings_open(
|
||||
state: State<'_, AppNode>,
|
||||
posting_id_hex: String,
|
||||
) -> Result<bool, String> {
|
||||
let node = get_node(&state).await;
|
||||
let posting_id = parse_node_id(&posting_id_hex)?;
|
||||
node.get_greetings_open(&posting_id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Open a URL in the user's default system browser.
|
||||
/// Desktop: spawns the platform opener (xdg-open / open / cmd start).
|
||||
/// Only https:// URLs are accepted to avoid being a generic command exec.
|
||||
|
|
@ -2063,6 +2333,28 @@ async fn get_seen_engagement(
|
|||
}))
|
||||
}
|
||||
|
||||
/// Comment count for the engagement badges, EXCLUDING greeting open-slot
|
||||
/// comments: greetings have their own dedicated badge/inbox, and counting
|
||||
/// them here would double-count every greeting into "My Posts" while
|
||||
/// pointing the user at a bio post where the comment renders as sealed
|
||||
/// ciphertext instead of at the Greetings inbox.
|
||||
fn non_greeting_comment_count(
|
||||
storage: &itsgoin_core::storage::Storage,
|
||||
post_id: &itsgoin_core::types::PostId,
|
||||
post: &itsgoin_core::types::Post,
|
||||
) -> u64 {
|
||||
let mut count = storage.get_comment_count(post_id).unwrap_or(0);
|
||||
if let Some(decl) = post.fof_gating.as_ref().and_then(|g| g.open_slot.as_ref()) {
|
||||
if matches!(decl.kind, itsgoin_core::types::OpenSlotKind::Greeting) {
|
||||
let greetings = storage
|
||||
.count_unexpired_open_slot_comments(post_id, decl.slot_index)
|
||||
.unwrap_or(0);
|
||||
count = count.saturating_sub(greetings);
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_badge_counts(
|
||||
state: State<'_, AppNode>,
|
||||
|
|
@ -2071,11 +2363,17 @@ async fn get_badge_counts(
|
|||
let node = get_node(&state).await;
|
||||
let storage = node.storage.get().await;
|
||||
|
||||
// "Own post" checks below are posting-class: posts are authored by our
|
||||
// posting identities (personas), never the network NodeId.
|
||||
let own_ids: Vec<itsgoin_core::types::NodeId> = storage.list_posting_identities()
|
||||
.unwrap_or_default()
|
||||
.into_iter().map(|p| p.node_id).collect();
|
||||
|
||||
// Feed badge: count non-DM posts from others newer than last_feed_view_ms
|
||||
let feed_posts = storage.get_feed().map_err(|e| e.to_string())?;
|
||||
let new_feed = feed_posts.iter()
|
||||
.filter(|(id, p, _vis)| {
|
||||
p.author != node.node_id
|
||||
!own_ids.contains(&p.author)
|
||||
&& p.timestamp_ms > last_feed_view_ms
|
||||
&& !matches!(
|
||||
storage.get_post_intent(id).ok().flatten(),
|
||||
|
|
@ -2088,18 +2386,20 @@ async fn get_badge_counts(
|
|||
let all_posts = storage.list_posts_reverse_chron().map_err(|e| e.to_string())?;
|
||||
let mut new_engagement = 0usize;
|
||||
for (id, post, _vis) in &all_posts {
|
||||
if post.author != node.node_id { continue; }
|
||||
if !own_ids.contains(&post.author) { continue; }
|
||||
// Skip DMs
|
||||
if matches!(
|
||||
storage.get_post_intent(id).ok().flatten(),
|
||||
Some(VisibilityIntent::Direct(_))
|
||||
) { continue; }
|
||||
let total_reacts: u64 = storage.get_reaction_counts(id, &node.node_id)
|
||||
let total_reacts: u64 = storage.get_reaction_counts(id, &own_ids)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|(_, count, _)| *count)
|
||||
.sum();
|
||||
let total_comments = storage.get_comment_count(id).unwrap_or(0);
|
||||
// Greeting open-slot comments are excluded — they have their own
|
||||
// dedicated Greetings badge (see non_greeting_comment_count).
|
||||
let total_comments = non_greeting_comment_count(&storage, id, post);
|
||||
if total_reacts > 0 || total_comments > 0 {
|
||||
let (seen_r, seen_c) = storage.get_seen_engagement(id).unwrap_or((0, 0));
|
||||
if total_reacts > seen_r as u64 || total_comments > seen_c as u64 {
|
||||
|
|
@ -2114,14 +2414,14 @@ async fn get_badge_counts(
|
|||
matches!(
|
||||
storage.get_post_intent(id).ok().flatten(),
|
||||
Some(VisibilityIntent::Direct(_))
|
||||
) || (p.author != node.node_id && matches!(
|
||||
) || (!own_ids.contains(&p.author) && matches!(
|
||||
storage.get_post_with_visibility(id).ok().flatten(),
|
||||
Some((_, PostVisibility::Encrypted { .. }))
|
||||
))
|
||||
});
|
||||
let mut seen_partners = std::collections::HashSet::new();
|
||||
for (_id, post, _vis) in dm_posts {
|
||||
let partner = if post.author == node.node_id {
|
||||
let partner = if own_ids.contains(&post.author) {
|
||||
// sent DM — skip for unread count
|
||||
continue;
|
||||
} else {
|
||||
|
|
@ -2139,16 +2439,22 @@ async fn get_badge_counts(
|
|||
let mut new_reacts = 0usize;
|
||||
let mut new_comments = 0usize;
|
||||
for (id, post, _vis) in &all_posts {
|
||||
if post.author != node.node_id { continue; }
|
||||
let total_reacts: u64 = storage.get_reaction_counts(id, &node.node_id)
|
||||
if !own_ids.contains(&post.author) { continue; }
|
||||
let total_reacts: u64 = storage.get_reaction_counts(id, &own_ids)
|
||||
.unwrap_or_default().iter().map(|(_, c, _)| *c).sum();
|
||||
let total_comments = storage.get_comment_count(id).unwrap_or(0);
|
||||
// Greeting open-slot comments excluded here too (dedicated badge).
|
||||
let total_comments = non_greeting_comment_count(&storage, id, post);
|
||||
let (seen_r, seen_c) = storage.get_seen_engagement(id).unwrap_or((0, 0));
|
||||
if total_reacts > seen_r as u64 { new_reacts += (total_reacts - seen_r as u64) as usize; }
|
||||
if total_comments > seen_c as u64 { new_comments += (total_comments - seen_c as u64) as usize; }
|
||||
}
|
||||
|
||||
Ok(BadgeCountsDto { new_feed, new_engagement, unread_messages, new_reacts, new_comments })
|
||||
// v0.8 (A3): greetings inbox size. Release the storage slot first —
|
||||
// list_greetings takes its own guard internally.
|
||||
drop(storage);
|
||||
let greetings = node.list_greetings().await.map(|g| g.len()).unwrap_or(0);
|
||||
|
||||
Ok(BadgeCountsDto { new_feed, new_engagement, unread_messages, new_reacts, new_comments, greetings })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -2994,6 +3300,8 @@ async fn export_data(
|
|||
}
|
||||
}
|
||||
};
|
||||
// node_id here is only used for the export file name; the post filter
|
||||
// inside export_data now iterates all posting identities itself.
|
||||
let result = itsgoin_core::export::export_data(
|
||||
&node.data_dir,
|
||||
&node.storage,
|
||||
|
|
@ -3083,11 +3391,13 @@ async fn import_public_posts(
|
|||
zip_path: String,
|
||||
) -> Result<String, String> {
|
||||
let node = get_node(&state).await;
|
||||
// Imported posts are re-authored as US — that must be a POSTING identity
|
||||
// (the network NodeId never authors posts).
|
||||
let result = itsgoin_core::import::import_public_posts(
|
||||
std::path::Path::new(&zip_path),
|
||||
&node.storage,
|
||||
&node.blob_store,
|
||||
&node.node_id,
|
||||
&node.default_posting_id,
|
||||
).await.map_err(|e| e.to_string())?;
|
||||
Ok(result.message)
|
||||
}
|
||||
|
|
@ -3141,12 +3451,14 @@ async fn import_merge_with_key(
|
|||
key_hex: String,
|
||||
) -> Result<String, String> {
|
||||
let node = get_node(&state).await;
|
||||
// Merged content is authored under a POSTING identity, never the
|
||||
// network NodeId.
|
||||
let result = itsgoin_core::import::merge_with_key(
|
||||
std::path::Path::new(&zip_path),
|
||||
&key_hex,
|
||||
&node.storage,
|
||||
&node.blob_store,
|
||||
&node.node_id,
|
||||
&node.default_posting_id,
|
||||
&node.secret_seed(),
|
||||
).await.map_err(|e| e.to_string())?;
|
||||
Ok(result.message)
|
||||
|
|
@ -3414,6 +3726,17 @@ pub fn run() {
|
|||
exit_app,
|
||||
get_session_relay_enabled,
|
||||
set_session_relay_enabled,
|
||||
register_persona,
|
||||
unregister_persona,
|
||||
get_registry_status,
|
||||
search_registry,
|
||||
get_greeting_slot,
|
||||
send_greeting,
|
||||
reply_to_greeting,
|
||||
list_greetings,
|
||||
dismiss_greeting,
|
||||
set_greetings_open,
|
||||
get_greetings_open,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue