use ::hex::ToHex;
use deltachat::context::Context;
use deltachat::securejoin;
use nostr::prelude::*;
use std::collections::HashSet;
use std::str::FromStr;
use crate::{bot::delta, db, Db, DeltaBotStatus, NostrBotStateTransitions, NostrBotStatus};
use rocket::{serde::json::Json, State};
use rocket_db_pools::Connection;
use crate::model::bot::delta::*;
use crate::model::bot::nostr::*;
use crate::model::api::{ApiError, ApiErrorLogged, ValidationError};
use crate::params::StartupParamsOptional;
use crate::util::oak::{CONFIG_KEY_NOSTR_BOT_OWNER_PUB_KEY, CONFIG_KEY_NOSTR_WS_ENDPOINT};
use crate::bot::ons::nip47::{Nip47InnerConfig, SecretConfig};
use crate::model::api::ApiError::GenericError;
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(
setup_email,
get_pairing_code,
enable_disable_ons,
ons3_get_amount,
ons3_update_amount,
nip47_create,
nip47_list,
nip47_delete,
nostr_setup_owner_pubkey_and_relay,
),
components(
schemas(
VerifiedEmailInfo, DeltaBotSetupInfo,
SecretConfig,
)
),
tags(
(name = "bot", description = "Bot-related APIs"),
),
)]
pub(crate) struct ApiDoc;
/// Configure the bot's email account
#[utoipa::path(
tag = "bot",
context_path = "/bot",
request_body = DeltaBotSetupInfo,
responses(
(status = 200, description = "Bot was successfully configured to use this email account"),
(status = 500, description = "Setup error")
)
)]
#[post("/email", data = "<setup_info>")]
pub(crate) async fn setup_email(
setup_info: Json<DeltaBotSetupInfo>,
bot_ctx: &State<Context>,
bot_status: &State<DeltaBotStatus>,
) -> Result<(), ApiErrorLogged> {
delta::configure(bot_ctx.inner(), setup_info.into_inner())
.await
.map(|_| {
info!("Configured email bot");
bot_status.inner().to_state_pending_restart()?;
Ok(())
})?
}
/// Get a pairing code
#[utoipa::path(
tag = "bot",
context_path = "/bot",
responses(
(status = 200, description = "Pairing code", body = String),
(status = 400, description = "When trying to get a pairing code for a bot that is already paired"),
(status = 500, description = "General error")
)
)]
#[get("/email")]
pub(crate) async fn get_pairing_code(
bot_ctx: &State<Context>,
bot_status: &State<DeltaBotStatus>,
) -> Result<String, ApiErrorLogged> {
if *bot_status.state.lock()? != DeltaBotState::PendingOwnerPairing {
return Err(ValidationError::of(
"Pairing code cannot be generated for this bot state",
))?;
}
securejoin::get_securejoin_qr(bot_ctx, None)
.await
.map(Ok)
.map_err(|e| GenericError(format!("Failed to generate pairing code: {e}")))?
}
/// Enable or disable Nostr bot modules
#[utoipa::path(
tag = "bot",
context_path = "/bot",
responses(
(status = 200, description = "Bot module was successfully enabled or disabled"),
(status = 500, description = "Setup error")
)
)]
#[post("/nostr/<ons_id>/<flag>")]
pub(crate) async fn enable_disable_ons(
ons_id: String,
flag: bool,
nostr_bot_status: &State<NostrBotStatus>,
mut db: Connection<Db>,
) -> Result<(), ApiErrorLogged> {
let ons_id_exists = nostr_bot_status
.ons
.lock()?
.iter()
.any(|ons_meta| ons_meta.id == ons_id);
match ons_id_exists {
true => {
nostr_bot_status.to_state_pending_restart()?;
crate::bot::ons::set_enabled(&mut db, ons_id, flag).await?;
Ok(())
}
false => Err(ValidationError::of("Unknown ons_id"))?,
}
}
/// Get the ons3 tipping amount
#[utoipa::path(
tag = "bot",
context_path = "/bot",
responses(
(status = 200, description = "The current tipping amount, in sats"),
)
)]
#[get("/nostr/ons3/tip_amount_sat")]
pub(crate) async fn ons3_get_amount(
composite_params: &State<StartupParamsOptional>,
) -> Result<String, ApiErrorLogged> {
Ok(composite_params.ons3.tip_amount_sat.to_string())
}
/// Update the ons3 tipping amount
#[utoipa::path(
tag = "bot",
context_path = "/bot",
request_body = Integer,
responses(
(status = 200, description = "Bot module was successfully updated"),
(status = 500, description = "Setup error")
)
)]
#[post("/nostr/ons3/tip_amount_sat", data = "<tip_amount_sat>")]
pub(crate) async fn ons3_update_amount(
tip_amount_sat: String,
nostr_bot_status: &State<NostrBotStatus>,
mut db: Connection<Db>,
) -> Result<(), ApiErrorLogged> {
let tip_amount_sat = u64::from_str(&tip_amount_sat).map_err(|e| {
ApiError::InvalidInput(ValidationError::of("Invalid input")).to_logged_with_debug(&e)
})?;
match tip_amount_sat > 0 {
true => {
nostr_bot_status.to_state_pending_restart()?;
let key = "ons3.tip_amount_sat";
db::config::config_write_or_update(&mut db, key, &tip_amount_sat.to_string()).await?;
Ok(())
}
false => Err(ValidationError::of("Amount must be bigger than zero"))?,
}
}
/// Create a new NIP-47 NWC secret
#[utoipa::path(
tag = "bot",
context_path = "/bot",
responses(
(status = 200, description = "Secret successfully created"),
(status = 500, description = "Setup error")
)
)]
#[post("/nostr/nip47")]
pub(crate) async fn nip47_create(
nostr_bot_status: &State<NostrBotStatus>,
spo: &State<StartupParamsOptional>,
db: Connection<Db>,
) -> Result<(), ApiErrorLogged> {
nostr_bot_status.ensure_nip47_module_enabled()?;
let new_secret = SecretConfig {
secret: SecretKey::new(&mut rand::thread_rng())
.secret_bytes()
.encode_hex(),
};
info!("Created new NIP-47 NWC secret: {new_secret:?}");
nostr_bot_status.to_state_pending_restart()?;
let mut inner_config: Nip47InnerConfig = crate::bot::ons::nip47::get_inner_config(spo)?;
inner_config.secrets.insert(new_secret);
crate::bot::ons::nip47::persist_inner_config(db, inner_config).await
}
/// List the NIP-47 NWC secrets
#[utoipa::path(
tag = "bot",
context_path = "/bot",
responses(
(status = 200, description = "Secrets successfully listed"),
(status = 500, description = "Setup error")
)
)]
#[get("/nostr/nip47")]
pub(crate) async fn nip47_list(
nostr_bot_status: &State<NostrBotStatus>,
spo: &State<StartupParamsOptional>,
) -> Result<Json<HashSet<SecretConfig>>, ApiErrorLogged> {
nostr_bot_status.ensure_nip47_module_enabled()?;
let inner_config: Nip47InnerConfig = crate::bot::ons::nip47::get_inner_config(spo)?;
Ok(Json::from(inner_config.secrets))
}
/// Delete a NWC secret
#[utoipa::path(
tag = "bot",
context_path = "/bot",
params(
("secret", description = "Secret to delete")
),
responses(
(status = 200, description = "Secret was successfully deleted"),
(status = 500, description = "Setup error")
)
)]
#[delete("/nostr/nip47/<secret>")]
pub(crate) async fn nip47_delete(
secret: String,
nostr_bot_status: &State<NostrBotStatus>,
spo: &State<StartupParamsOptional>,
db: Connection<Db>,
) -> Result<(), ApiErrorLogged> {
nostr_bot_status.ensure_nip47_module_enabled()?;
info!("Deleting NIP-47 NWC secret: {secret:?}");
let mut inner_config: Nip47InnerConfig = crate::bot::ons::nip47::get_inner_config(spo)?;
match inner_config
.secrets
.iter()
.find(|&s| s.secret == secret)
.cloned()
{
None => Err(GenericError("Secret does not exist".into()).into()),
Some(s) => {
nostr_bot_status.to_state_pending_restart()?;
inner_config.secrets.remove(&s);
crate::bot::ons::nip47::persist_inner_config(db, inner_config).await
}
}
}
/// Configure the owner's pubkey
#[utoipa::path(
tag = "bot",
context_path = "/bot",
request_body = NostrBotSetupInfo,
responses(
(status = 200, description = "The owner pubkey and the relay URL are valid and were successfully saved"),
(status = 400, description = "Invalid input"),
(status = 500, description = "Setup error")
)
)]
#[post("/nostr", data = "<setup_info>")]
pub(crate) async fn nostr_setup_owner_pubkey_and_relay(
setup_info: Json<NostrBotSetupInfo>,
nostr_bot_status: &State<NostrBotStatus>,
mut db: Connection<Db>,
) -> Result<(), ApiErrorLogged> {
match *nostr_bot_status.state.lock()? {
NostrBotState::PendingOwnerPairing | NostrBotState::Running => {}
_ => {
return Err(ValidationError::of(
"Owner pubkey and relay cannot be configured for this bot state",
))?
}
}
let info = setup_info.into_inner();
// Validate key
let pk = match XOnlyPublicKey::from_bech32(&info.pk_hex_or_npub) {
Ok(npub) => npub,
Err(_) => {
info!("Owner pk is not an npub, trying hex");
XOnlyPublicKey::from_str(&info.pk_hex_or_npub).map_err(|e| {
ApiError::InvalidInput(ValidationError::of(
"Invalid pubkey, it is neither a hex nor an npub",
))
.to_logged_with_debug(&e)
})?
}
};
// Validate relay
let relay = Url::parse(&info.relay).map_err(|e| {
ApiError::InvalidInput(ValidationError::of("Invalid relay URL")).to_logged_with_debug(&e)
})?;
// Update bot status
// We call this before persisting to DB, because this may not be allowed for some bot states,
// in which case this will fail
nostr_bot_status.to_state_pending_restart()?;
// Persist hex pk to DB
let pk_hex: String = pk.serialize().encode_hex();
db::config::config_write_or_update(&mut db, CONFIG_KEY_NOSTR_BOT_OWNER_PUB_KEY, &pk_hex)
.await?;
// Persist relay URL to DB
db::config::config_write_or_update(&mut db, CONFIG_KEY_NOSTR_WS_ENDPOINT, relay.as_str())
.await?;
info!("Persisted owner pubkey and relay");
Ok(())
}