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
|
|
@ -618,6 +618,73 @@ async fn main() -> anyhow::Result<()> {
|
||||||
Err(e) => println!("Error: {}", e),
|
Err(e) => println!("Error: {}", e),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"personas" => match node.list_posting_identities().await {
|
||||||
|
Ok(identities) => {
|
||||||
|
let default = {
|
||||||
|
let s = node.storage.get().await;
|
||||||
|
s.get_default_posting_id().ok().flatten()
|
||||||
|
};
|
||||||
|
if identities.is_empty() {
|
||||||
|
println!("(no posting identities)");
|
||||||
|
}
|
||||||
|
for id in identities {
|
||||||
|
let mark = if Some(id.node_id) == default { " *" } else { "" };
|
||||||
|
let name = if id.display_name.is_empty() { "(unnamed)" } else { &id.display_name };
|
||||||
|
println!(" {} {} {}{}", &hex::encode(id.node_id)[..12], name, id.created_at, mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => println!("Error: {}", e),
|
||||||
|
},
|
||||||
|
|
||||||
|
"create-persona" => {
|
||||||
|
let name = arg.unwrap_or("").to_string();
|
||||||
|
match node.create_posting_identity(name).await {
|
||||||
|
Ok(id) => {
|
||||||
|
println!("Created posting identity: {}", hex::encode(id.node_id));
|
||||||
|
}
|
||||||
|
Err(e) => println!("Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"delete-persona" => {
|
||||||
|
if let Some(id_hex) = arg {
|
||||||
|
match itsgoin_core::parse_node_id_hex(id_hex) {
|
||||||
|
Ok(nid) => match node.delete_posting_identity(&nid).await {
|
||||||
|
Ok(()) => println!("Deleted posting identity"),
|
||||||
|
Err(e) => println!("Error: {}", e),
|
||||||
|
},
|
||||||
|
Err(e) => println!("Invalid node id: {}", e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Usage: delete-persona <node_id>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"post-as" => {
|
||||||
|
if let Some(rest) = arg {
|
||||||
|
let parts: Vec<&str> = rest.splitn(2, ' ').collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
println!("Usage: post-as <posting_id_hex> <text>");
|
||||||
|
} else {
|
||||||
|
match itsgoin_core::parse_node_id_hex(parts[0]) {
|
||||||
|
Ok(nid) => {
|
||||||
|
match node
|
||||||
|
.create_post_as(&nid, parts[1].to_string(),
|
||||||
|
itsgoin_core::types::VisibilityIntent::Public, vec![])
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((id, _, _)) => println!("Posted as persona! ID: {}", hex::encode(id)),
|
||||||
|
Err(e) => println!("Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => println!("Invalid node id: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Usage: post-as <posting_id_hex> <text>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"export-key" => {
|
"export-key" => {
|
||||||
match node.export_identity_hex() {
|
match node.export_identity_hex() {
|
||||||
Ok(hex_key) => {
|
Ok(hex_key) => {
|
||||||
|
|
|
||||||
|
|
@ -577,6 +577,66 @@ impl Node {
|
||||||
self.delivery_budget_remaining.load(AtomicOrdering::Relaxed)
|
self.delivery_budget_remaining.load(AtomicOrdering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Posting identities (multi-persona) ----
|
||||||
|
|
||||||
|
/// List all posting identities held by this device.
|
||||||
|
pub async fn list_posting_identities(&self) -> anyhow::Result<Vec<crate::types::PostingIdentity>> {
|
||||||
|
let s = self.storage.get().await;
|
||||||
|
s.list_posting_identities()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new posting identity with a fresh ed25519 key. Auto-follows
|
||||||
|
/// the new identity so its own posts show in the merged feed.
|
||||||
|
pub async fn create_posting_identity(
|
||||||
|
&self,
|
||||||
|
display_name: String,
|
||||||
|
) -> anyhow::Result<crate::types::PostingIdentity> {
|
||||||
|
let key = iroh::SecretKey::generate(&mut rand::rng());
|
||||||
|
let seed: [u8; 32] = key.to_bytes();
|
||||||
|
let node_id: NodeId = *key.public().as_bytes();
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)?
|
||||||
|
.as_millis() as u64;
|
||||||
|
let identity = crate::types::PostingIdentity {
|
||||||
|
node_id,
|
||||||
|
secret_seed: seed,
|
||||||
|
display_name,
|
||||||
|
created_at: now,
|
||||||
|
};
|
||||||
|
let s = self.storage.get().await;
|
||||||
|
s.upsert_posting_identity(&identity)?;
|
||||||
|
// Auto-follow this persona so its own posts reach its own feed.
|
||||||
|
s.add_follow(&node_id)?;
|
||||||
|
Ok(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a posting identity. Refuses to delete the currently default
|
||||||
|
/// posting identity unless the caller has already switched the default.
|
||||||
|
pub async fn delete_posting_identity(&self, node_id: &NodeId) -> anyhow::Result<()> {
|
||||||
|
let s = self.storage.get().await;
|
||||||
|
if let Some(default) = s.get_default_posting_id()? {
|
||||||
|
if default == *node_id {
|
||||||
|
anyhow::bail!("cannot delete the default posting identity; set a different default first");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.delete_posting_identity(node_id)?;
|
||||||
|
// Best-effort: remove the auto-follow row for this persona.
|
||||||
|
let _ = s.remove_follow(node_id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch the default posting identity. Takes effect on next restart for
|
||||||
|
/// the Node's cached fields, but new posts created via create_post_as can
|
||||||
|
/// already use the new identity immediately.
|
||||||
|
pub async fn set_default_posting_identity(&self, node_id: &NodeId) -> anyhow::Result<()> {
|
||||||
|
let s = self.storage.get().await;
|
||||||
|
if s.get_posting_identity(node_id)?.is_none() {
|
||||||
|
anyhow::bail!("unknown posting identity");
|
||||||
|
}
|
||||||
|
s.set_default_posting_id(node_id)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Identity export/import ----
|
// ---- Identity export/import ----
|
||||||
|
|
||||||
pub fn secret_seed(&self) -> [u8; 32] {
|
pub fn secret_seed(&self) -> [u8; 32] {
|
||||||
|
|
@ -638,6 +698,47 @@ impl Node {
|
||||||
content: String,
|
content: String,
|
||||||
intent: VisibilityIntent,
|
intent: VisibilityIntent,
|
||||||
attachment_data: Vec<(Vec<u8>, String)>,
|
attachment_data: Vec<(Vec<u8>, String)>,
|
||||||
|
) -> anyhow::Result<(PostId, Post, PostVisibility)> {
|
||||||
|
self.create_post_inner(
|
||||||
|
&self.default_posting_id,
|
||||||
|
&self.default_posting_secret,
|
||||||
|
content,
|
||||||
|
intent,
|
||||||
|
attachment_data,
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a post authored by a specific posting identity held by this
|
||||||
|
/// device. Looks up the posting secret and routes through the same post
|
||||||
|
/// creation pipeline as the default.
|
||||||
|
pub async fn create_post_as(
|
||||||
|
&self,
|
||||||
|
posting_id: &NodeId,
|
||||||
|
content: String,
|
||||||
|
intent: VisibilityIntent,
|
||||||
|
attachment_data: Vec<(Vec<u8>, String)>,
|
||||||
|
) -> anyhow::Result<(PostId, Post, PostVisibility)> {
|
||||||
|
let identity = {
|
||||||
|
let s = self.storage.get().await;
|
||||||
|
s.get_posting_identity(posting_id)?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("unknown posting identity"))?
|
||||||
|
};
|
||||||
|
self.create_post_inner(
|
||||||
|
&identity.node_id,
|
||||||
|
&identity.secret_seed,
|
||||||
|
content,
|
||||||
|
intent,
|
||||||
|
attachment_data,
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_post_inner(
|
||||||
|
&self,
|
||||||
|
posting_id: &NodeId,
|
||||||
|
posting_secret: &[u8; 32],
|
||||||
|
content: String,
|
||||||
|
intent: VisibilityIntent,
|
||||||
|
attachment_data: Vec<(Vec<u8>, String)>,
|
||||||
) -> anyhow::Result<(PostId, Post, PostVisibility)> {
|
) -> anyhow::Result<(PostId, Post, PostVisibility)> {
|
||||||
// Validate attachments
|
// Validate attachments
|
||||||
if attachment_data.len() > 4 {
|
if attachment_data.len() > 4 {
|
||||||
|
|
@ -726,7 +827,7 @@ impl Node {
|
||||||
EncryptionMode::Public => (content, PostVisibility::Public),
|
EncryptionMode::Public => (content, PostVisibility::Public),
|
||||||
EncryptionMode::Recipient { cek, recipients } => {
|
EncryptionMode::Recipient { cek, recipients } => {
|
||||||
let (encrypted, wrapped_keys) =
|
let (encrypted, wrapped_keys) =
|
||||||
crypto::encrypt_post_with_cek(&content, &cek, &self.default_posting_secret, &self.node_id, &recipients)?;
|
crypto::encrypt_post_with_cek(&content, &cek, posting_secret, posting_id, &recipients)?;
|
||||||
(
|
(
|
||||||
encrypted,
|
encrypted,
|
||||||
PostVisibility::Encrypted {
|
PostVisibility::Encrypted {
|
||||||
|
|
@ -749,7 +850,7 @@ impl Node {
|
||||||
};
|
};
|
||||||
|
|
||||||
let post = Post {
|
let post = Post {
|
||||||
author: self.default_posting_id,
|
author: *posting_id,
|
||||||
content: final_content,
|
content: final_content,
|
||||||
attachments,
|
attachments,
|
||||||
timestamp_ms: now,
|
timestamp_ms: now,
|
||||||
|
|
@ -761,7 +862,7 @@ impl Node {
|
||||||
let storage = self.storage.get().await;
|
let storage = self.storage.get().await;
|
||||||
storage.store_post_with_intent(&post_id, &post, &visibility, &intent)?;
|
storage.store_post_with_intent(&post_id, &post, &visibility, &intent)?;
|
||||||
for att in &post.attachments {
|
for att in &post.attachments {
|
||||||
storage.record_blob(&att.cid, &post_id, &self.node_id, att.size_bytes, &att.mime_type, now)?;
|
storage.record_blob(&att.cid, &post_id, posting_id, att.size_bytes, &att.mime_type, now)?;
|
||||||
// Auto-pin own blobs so they're never evicted before foreign content
|
// Auto-pin own blobs so they're never evicted before foreign content
|
||||||
let _ = storage.pin_blob(&att.cid);
|
let _ = storage.pin_blob(&att.cid);
|
||||||
}
|
}
|
||||||
|
|
@ -795,7 +896,7 @@ impl Node {
|
||||||
|
|
||||||
let blob_header = crate::types::BlobHeader {
|
let blob_header = crate::types::BlobHeader {
|
||||||
post_id,
|
post_id,
|
||||||
author: self.default_posting_id,
|
author: *posting_id,
|
||||||
reactions: vec![],
|
reactions: vec![],
|
||||||
comments: vec![],
|
comments: vec![],
|
||||||
policy: Default::default(),
|
policy: Default::default(),
|
||||||
|
|
@ -806,19 +907,19 @@ impl Node {
|
||||||
prior_author: None,
|
prior_author: None,
|
||||||
};
|
};
|
||||||
let header_json = serde_json::to_string(&blob_header)?;
|
let header_json = serde_json::to_string(&blob_header)?;
|
||||||
storage.store_blob_header(&post_id, &self.node_id, &header_json, now)?;
|
storage.store_blob_header(&post_id, posting_id, &header_json, now)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build and store CDN manifests for blobs
|
// Build and store CDN manifests for blobs
|
||||||
if !post.attachments.is_empty() {
|
if !post.attachments.is_empty() {
|
||||||
let storage = self.storage.get().await;
|
let storage = self.storage.get().await;
|
||||||
let (previous, _following) = storage.get_author_post_neighborhood(&self.node_id, now, 10)?;
|
let (previous, _following) = storage.get_author_post_neighborhood(posting_id, now, 10)?;
|
||||||
drop(storage);
|
drop(storage);
|
||||||
|
|
||||||
let manifest = crate::types::AuthorManifest {
|
let manifest = crate::types::AuthorManifest {
|
||||||
post_id,
|
post_id,
|
||||||
author: self.default_posting_id,
|
author: *posting_id,
|
||||||
author_addresses: self.network.our_addresses(),
|
author_addresses: self.network.our_addresses(),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
|
|
@ -826,7 +927,7 @@ impl Node {
|
||||||
following_posts: vec![],
|
following_posts: vec![],
|
||||||
signature: vec![],
|
signature: vec![],
|
||||||
};
|
};
|
||||||
let sig = crypto::sign_manifest(&self.default_posting_secret, &manifest);
|
let sig = crypto::sign_manifest(posting_secret, &manifest);
|
||||||
let mut manifest = manifest;
|
let mut manifest = manifest;
|
||||||
manifest.signature = sig;
|
manifest.signature = sig;
|
||||||
|
|
||||||
|
|
@ -834,17 +935,17 @@ impl Node {
|
||||||
{
|
{
|
||||||
let storage = self.storage.get().await;
|
let storage = self.storage.get().await;
|
||||||
for att in &post.attachments {
|
for att in &post.attachments {
|
||||||
storage.store_cdn_manifest(&att.cid, &manifest_json, &self.node_id, now)?;
|
storage.store_cdn_manifest(&att.cid, &manifest_json, posting_id, now)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update previous posts' manifests to include this new post as a following_post
|
// Update previous posts' manifests to include this new post as a following_post
|
||||||
self.update_neighbor_manifests(&post_id, now).await;
|
self.update_neighbor_manifests_as(posting_id, posting_secret, &post_id, now).await;
|
||||||
|
|
||||||
// Push updated manifests to downstream peers
|
// Push updated manifests to downstream peers
|
||||||
let manifests_to_push = {
|
let manifests_to_push = {
|
||||||
let storage = self.storage.get().await;
|
let storage = self.storage.get().await;
|
||||||
storage.get_manifests_for_author_blobs(&self.node_id).unwrap_or_default()
|
storage.get_manifests_for_author_blobs(posting_id).unwrap_or_default()
|
||||||
};
|
};
|
||||||
let our_addrs = self.network.our_addresses();
|
let our_addrs = self.network.our_addresses();
|
||||||
for (push_cid, push_json) in &manifests_to_push {
|
for (push_cid, push_json) in &manifests_to_push {
|
||||||
|
|
@ -873,9 +974,15 @@ impl Node {
|
||||||
|
|
||||||
/// Update the manifests of recent prior posts to include a newly created post
|
/// Update the manifests of recent prior posts to include a newly created post
|
||||||
/// in their following_posts list. Re-signs each updated manifest.
|
/// in their following_posts list. Re-signs each updated manifest.
|
||||||
async fn update_neighbor_manifests(&self, new_post_id: &PostId, new_timestamp_ms: u64) {
|
async fn update_neighbor_manifests_as(
|
||||||
|
&self,
|
||||||
|
posting_id: &NodeId,
|
||||||
|
posting_secret: &[u8; 32],
|
||||||
|
new_post_id: &PostId,
|
||||||
|
new_timestamp_ms: u64,
|
||||||
|
) {
|
||||||
let storage = self.storage.get().await;
|
let storage = self.storage.get().await;
|
||||||
let manifests = match storage.get_manifests_for_author_blobs(&self.node_id) {
|
let manifests = match storage.get_manifests_for_author_blobs(posting_id) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to get manifests for neighbor update: {}", e);
|
warn!("Failed to get manifests for neighbor update: {}", e);
|
||||||
|
|
@ -909,14 +1016,14 @@ impl Node {
|
||||||
}
|
}
|
||||||
manifest.following_posts.push(new_entry.clone());
|
manifest.following_posts.push(new_entry.clone());
|
||||||
manifest.updated_at = new_timestamp_ms;
|
manifest.updated_at = new_timestamp_ms;
|
||||||
manifest.signature = crypto::sign_manifest(&self.default_posting_secret, &manifest);
|
manifest.signature = crypto::sign_manifest(posting_secret, &manifest);
|
||||||
|
|
||||||
let updated_json = match serde_json::to_string(&manifest) {
|
let updated_json = match serde_json::to_string(&manifest) {
|
||||||
Ok(j) => j,
|
Ok(j) => j,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
let storage = self.storage.get().await;
|
let storage = self.storage.get().await;
|
||||||
let _ = storage.store_cdn_manifest(&cid, &updated_json, &self.node_id, new_timestamp_ms);
|
let _ = storage.store_cdn_manifest(&cid, &updated_json, posting_id, new_timestamp_ms);
|
||||||
drop(storage);
|
drop(storage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -475,6 +475,108 @@ async fn create_post_with_files(
|
||||||
Ok(post_to_dto(&id, &post, &vis, decrypted.as_deref(), &node).await)
|
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).
|
/// 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
|
/// 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.
|
/// via the asset protocol — the frontend must use IPC-based get_blob instead.
|
||||||
|
|
@ -2760,6 +2862,11 @@ pub fn run() {
|
||||||
set_profile,
|
set_profile,
|
||||||
create_post,
|
create_post,
|
||||||
create_post_with_files,
|
create_post_with_files,
|
||||||
|
create_post_as,
|
||||||
|
list_posting_identities,
|
||||||
|
create_posting_identity,
|
||||||
|
delete_posting_identity,
|
||||||
|
set_default_posting_identity,
|
||||||
get_blob,
|
get_blob,
|
||||||
get_blob_path,
|
get_blob_path,
|
||||||
save_and_open_blob,
|
save_and_open_blob,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue