Phase 5 (0.6.4-beta) backend: multi-persona creation + post-as
Users can now hold multiple posting identities on one device and publish content under any of them. Each persona has its own ed25519 key; peers see them as distinct authors with no link back to the device's network identity. Node methods: - list_posting_identities() -> Vec<PostingIdentity> - create_posting_identity(display_name) — generates a fresh ed25519 key, persists, auto-follows self - delete_posting_identity(node_id) — refuses to delete the default - set_default_posting_identity(node_id) — validates identity exists; Node's cached default_posting_id/secret picks up on next restart - create_post_as(posting_id, content, intent, attachments) — routes through a shared create_post_inner that takes posting_id + posting_secret as parameters Post creation pipeline: - create_post_with_visibility now delegates to create_post_inner using default_posting_id/secret - create_post_inner threads posting_id / posting_secret through every content-signing, encryption, manifest, blob-header, and CDN-manifest step — the persona is fully honored end to end - update_neighbor_manifests now takes a posting_id param too, so posts from persona X only update neighbor manifests for X's own prior posts Tauri IPC: - list_posting_identities / create_posting_identity / delete_posting_identity / set_default_posting_identity - create_post_as with posting_id_hex + the same visibility params as create_post CLI: - personas / create-persona <name> / delete-persona <id> - post-as <posting_id> <text> Smoke-tested two-persona scenario: - A creates "Work" persona; posts from default and Work - B follows both; pulls from A; gets all three posts - Authors are AB84BA... (Work) and 7CD949... (default) — distinct on the wire Frontend UX (Settings > Personas, compose picker, filter pills, merged feed labels) is scoped as a separate commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ce4b989b17
commit
7bdb2eb736
3 changed files with 296 additions and 15 deletions
|
|
@ -475,6 +475,108 @@ async fn create_post_with_files(
|
|||
Ok(post_to_dto(&id, &post, &vis, decrypted.as_deref(), &node).await)
|
||||
}
|
||||
|
||||
/// Posting identity DTO for IPC.
|
||||
#[derive(serde::Serialize)]
|
||||
struct PostingIdentityDto {
|
||||
#[serde(rename = "nodeId")]
|
||||
node_id: String,
|
||||
#[serde(rename = "displayName")]
|
||||
display_name: String,
|
||||
#[serde(rename = "createdAt")]
|
||||
created_at: u64,
|
||||
#[serde(rename = "isDefault")]
|
||||
is_default: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn list_posting_identities(
|
||||
state: State<'_, AppNode>,
|
||||
) -> Result<Vec<PostingIdentityDto>, String> {
|
||||
let node = get_node(&state).await;
|
||||
let identities = node.list_posting_identities().await.map_err(|e| e.to_string())?;
|
||||
let default = {
|
||||
let s = node.storage.get().await;
|
||||
s.get_default_posting_id().ok().flatten()
|
||||
};
|
||||
Ok(identities
|
||||
.into_iter()
|
||||
.map(|id| PostingIdentityDto {
|
||||
node_id: hex::encode(id.node_id),
|
||||
display_name: id.display_name,
|
||||
created_at: id.created_at,
|
||||
is_default: Some(id.node_id) == default,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_posting_identity(
|
||||
state: State<'_, AppNode>,
|
||||
display_name: String,
|
||||
) -> Result<PostingIdentityDto, String> {
|
||||
let node = get_node(&state).await;
|
||||
let id = node.create_posting_identity(display_name).await.map_err(|e| e.to_string())?;
|
||||
Ok(PostingIdentityDto {
|
||||
node_id: hex::encode(id.node_id),
|
||||
display_name: id.display_name,
|
||||
created_at: id.created_at,
|
||||
is_default: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete_posting_identity(
|
||||
state: State<'_, AppNode>,
|
||||
node_id_hex: String,
|
||||
) -> Result<(), String> {
|
||||
let node = get_node(&state).await;
|
||||
let nid = itsgoin_core::parse_node_id_hex(&node_id_hex).map_err(|e| e.to_string())?;
|
||||
node.delete_posting_identity(&nid).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_default_posting_identity(
|
||||
state: State<'_, AppNode>,
|
||||
node_id_hex: String,
|
||||
) -> Result<(), String> {
|
||||
let node = get_node(&state).await;
|
||||
let nid = itsgoin_core::parse_node_id_hex(&node_id_hex).map_err(|e| e.to_string())?;
|
||||
node.set_default_posting_identity(&nid).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Create a post signed by a specific posting identity instead of the default.
|
||||
#[tauri::command]
|
||||
async fn create_post_as(
|
||||
state: State<'_, AppNode>,
|
||||
posting_id_hex: String,
|
||||
content: String,
|
||||
visibility: Option<String>,
|
||||
circle_name: Option<String>,
|
||||
recipient_hex: Option<String>,
|
||||
) -> Result<PostDto, String> {
|
||||
let node = get_node(&state).await;
|
||||
let posting_id = itsgoin_core::parse_node_id_hex(&posting_id_hex).map_err(|e| e.to_string())?;
|
||||
let intent = match visibility.as_deref() {
|
||||
Some("friends") => VisibilityIntent::Friends,
|
||||
Some("circle") => {
|
||||
let name = circle_name.ok_or("circle_name required for circle visibility")?;
|
||||
VisibilityIntent::Circle(name)
|
||||
}
|
||||
Some("direct") => {
|
||||
let hex = recipient_hex.ok_or("recipient_hex required for direct visibility")?;
|
||||
let nid = itsgoin_core::parse_node_id_hex(&hex).map_err(|e| e.to_string())?;
|
||||
VisibilityIntent::Direct(vec![nid])
|
||||
}
|
||||
_ => VisibilityIntent::Public,
|
||||
};
|
||||
let (id, post, vis) = node
|
||||
.create_post_as(&posting_id, content, intent, vec![])
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let decrypted = decrypt_just_created(&node, &post, &vis).await;
|
||||
Ok(post_to_dto(&id, &post, &vis, decrypted.as_deref(), &node).await)
|
||||
}
|
||||
|
||||
/// Return the filesystem path of a blob if it exists locally (for streaming video/media).
|
||||
/// For non-public (encrypted) posts, returns None since raw encrypted blobs can't be served
|
||||
/// via the asset protocol — the frontend must use IPC-based get_blob instead.
|
||||
|
|
@ -2760,6 +2862,11 @@ pub fn run() {
|
|||
set_profile,
|
||||
create_post,
|
||||
create_post_with_files,
|
||||
create_post_as,
|
||||
list_posting_identities,
|
||||
create_posting_identity,
|
||||
delete_posting_identity,
|
||||
set_default_posting_identity,
|
||||
get_blob,
|
||||
get_blob_path,
|
||||
save_and_open_blob,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue