add encrypted Nostr session sharing (#8922)

Signed-off-by: callebtc <93376500+callebtc@users.noreply.github.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
callebtc
2026-05-12 22:06:44 -05:00
committed by GitHub
parent 8c36ba86c6
commit dbbee1cdbf
17 changed files with 1469 additions and 27 deletions
+5
View File
@@ -443,6 +443,8 @@ derive_utoipa!(IconTheme as IconThemeSchema);
super::routes::session::delete_session,
super::routes::session::export_session,
super::routes::session::import_session,
super::routes::session::share_session_nostr,
super::routes::session::import_session_nostr,
super::routes::session::update_session_user_recipe_values,
super::routes::session::fork_session,
super::routes::session::get_session_extensions,
@@ -512,6 +514,9 @@ derive_utoipa!(IconTheme as IconThemeSchema);
super::routes::session_events::SessionReplyResponse,
super::routes::session_events::CancelRequest,
super::routes::session::ImportSessionRequest,
super::routes::session::ShareSessionNostrRequest,
super::routes::session::ShareSessionNostrResponse,
super::routes::session::ImportSessionNostrRequest,
super::routes::session::SessionListResponse,
super::routes::session::UpdateSessionNameRequest,
super::routes::session::UpdateSessionUserRecipeValuesRequest,
+104
View File
@@ -11,6 +11,7 @@ use axum::{
};
use goose::agents::ExtensionConfig;
use goose::recipe::Recipe;
use goose::session::nostr_share;
use goose::session::session_manager::{SessionInsights, SessionType};
use goose::session::{EnabledExtensionsState, Session};
use serde::{Deserialize, Serialize};
@@ -50,6 +51,28 @@ pub struct ImportSessionRequest {
json: String,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ShareSessionNostrRequest {
#[serde(default)]
relays: Vec<String>,
}
#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ShareSessionNostrResponse {
deeplink: String,
nevent: String,
event_id: String,
relays: Vec<String>,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportSessionNostrRequest {
deeplink: String,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ForkRequest {
@@ -364,6 +387,79 @@ async fn import_session(
Ok(Json(session))
}
#[utoipa::path(
post,
path = "/sessions/{session_id}/share/nostr",
request_body = ShareSessionNostrRequest,
params(
("session_id" = String, Path, description = "Unique identifier for the session")
),
responses(
(status = 200, description = "Session shared to Nostr successfully", body = ShareSessionNostrResponse),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 404, description = "Session not found"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Session Management"
)]
async fn share_session_nostr(
State(state): State<Arc<AppState>>,
Path(session_id): Path<String>,
Json(request): Json<ShareSessionNostrRequest>,
) -> Result<Json<ShareSessionNostrResponse>, StatusCode> {
let exported = state
.session_manager()
.export_session(&session_id)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
let relays = nostr_share::resolve_relays(request.relays, goose::config::Config::global());
let share = nostr_share::publish_session_json(&exported, relays)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(ShareSessionNostrResponse {
deeplink: share.deeplink,
nevent: share.nevent,
event_id: share.event_id,
relays: share.relays,
}))
}
#[utoipa::path(
post,
path = "/sessions/import/nostr",
request_body = ImportSessionNostrRequest,
responses(
(status = 200, description = "Nostr shared session imported successfully", body = Session),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 400, description = "Bad request - Invalid Nostr share link"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Session Management"
)]
async fn import_session_nostr(
State(state): State<Arc<AppState>>,
Json(request): Json<ImportSessionNostrRequest>,
) -> Result<Json<Session>, StatusCode> {
let json = nostr_share::import_session_json_from_deeplink(&request.deeplink)
.await
.map_err(|_| StatusCode::BAD_REQUEST)?;
let session = state
.session_manager()
.import_session(&json, Some(SessionType::User))
.await
.map_err(|_| StatusCode::BAD_REQUEST)?;
Ok(Json(session))
}
#[utoipa::path(
post,
path = "/sessions/{session_id}/fork",
@@ -505,10 +601,18 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/sessions/{session_id}", get(get_session))
.route("/sessions/{session_id}", delete(delete_session))
.route("/sessions/{session_id}/export", get(export_session))
.route(
"/sessions/{session_id}/share/nostr",
post(share_session_nostr).layer(DefaultBodyLimit::max(25 * 1024 * 1024)),
)
.route(
"/sessions/import",
post(import_session).layer(DefaultBodyLimit::max(25 * 1024 * 1024)),
)
.route(
"/sessions/import/nostr",
post(import_session_nostr).layer(DefaultBodyLimit::max(25 * 1024 * 1024)),
)
.route("/sessions/insights", get(get_session_insights))
.route("/sessions/{session_id}/name", put(update_session_name))
.route(