Skip to content
This repository has been archived by the owner on Feb 3, 2025. It is now read-only.

Commit

Permalink
Get dm convo from primal
Browse files Browse the repository at this point in the history
  • Loading branch information
benthecarman committed Jan 22, 2024
1 parent 005493a commit 5ffef58
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 8 deletions.
139 changes: 138 additions & 1 deletion mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ use crate::{nostr::NostrManager, utils::sleep};
use ::nostr::key::XOnlyPublicKey;
use ::nostr::nips::nip57;
use ::nostr::prelude::ZapRequestData;
use ::nostr::{JsonUtil, Kind};
use ::nostr::{Event, JsonUtil, Kind};
use async_lock::RwLock;
use bdk_chain::ConfirmationTime;
use bip39::Mnemonic;
Expand Down Expand Up @@ -423,6 +423,31 @@ pub struct MutinyWalletConfigBuilder {
skip_hodl_invoices: bool,
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct DirectMessage {
pub from: XOnlyPublicKey,
pub to: XOnlyPublicKey,
pub message: String,
pub date: u64,
}

impl PartialOrd for DirectMessage {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}

impl Ord for DirectMessage {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
// order by date, then the message, the keys
self.date
.cmp(&other.date)
.then_with(|| self.message.cmp(&other.message))
.then_with(|| self.from.cmp(&other.from))
.then_with(|| self.to.cmp(&other.to))
}
}

impl MutinyWalletConfigBuilder {
pub fn new(xprivkey: ExtendedPrivKey) -> MutinyWalletConfigBuilder {
MutinyWalletConfigBuilder {
Expand Down Expand Up @@ -1437,6 +1462,64 @@ impl<S: MutinyStorage> MutinyWallet<S> {
Ok(())
}

/// Get dm conversation between us and given npub
/// Returns a vector of messages sorted by newest first
pub async fn get_dm_conversation(
&self,
primal_url: Option<&str>,
npub: XOnlyPublicKey,
limit: u64,
until: Option<u64>,
) -> Result<Vec<DirectMessage>, MutinyError> {
let url = primal_url.unwrap_or("https://primal-cache.mutinywallet.com/api");
let client = reqwest::Client::new();

// api is a little weird, has sender and receiver but still gives full conversation
let body = match until {
Some(until) => {
json!(["get_directmsgs", { "sender": npub.to_hex(), "receiver": self.nostr.public_key.to_hex(), "limit": limit, "until": until }])
}
None => {
json!(["get_directmsgs", { "sender": npub.to_hex(), "receiver": self.nostr.public_key.to_hex(), "limit": limit, "since": 0 }])
}
};
let data: Vec<Value> = Self::primal_request(&client, url, body).await?;

let mut messages = Vec::with_capacity(data.len());
for d in data {
let event = Event::from_value(d)
.ok()
.filter(|e| e.kind == Kind::EncryptedDirectMessage);

if let Some(event) = event {
// verify signature
if event.verify().is_err() {
continue;
}

let message = self.nostr.decrypt_dm(npub, &event.content).await?;

let to = if event.pubkey == npub {
self.nostr.public_key
} else {
npub
};
let dm = DirectMessage {
from: event.pubkey,
to,
message,
date: event.created_at.as_u64(),
};
messages.push(dm);
}
}

// sort messages, newest first
messages.sort_by(|a, b| b.cmp(a));

Ok(messages)
}

/// Stops all of the nodes and background processes.
/// Returns after node has been stopped.
pub async fn stop(&self) -> Result<(), MutinyError> {
Expand Down Expand Up @@ -1972,8 +2055,11 @@ mod tests {
use crate::test_utils::*;

use crate::labels::{Contact, LabelStorage};
use crate::nostr::NostrKeySource;
use crate::storage::{MemoryStorage, MutinyStorage};
use crate::utils::parse_npub;
use nostr::key::FromSkStr;
use nostr::Keys;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};

wasm_bindgen_test_configure!(run_in_browser);
Expand Down Expand Up @@ -2224,4 +2310,55 @@ mod tests {
assert!(contact.ln_address.is_some());
assert_ne!(contact.name, incorrect_name);
}

#[test]
async fn get_dm_conversation_test() {
// test nsec I made and sent dms to
let nsec =
Keys::from_sk_str("nsec1w2cy7vmq8urw9ae6wjaujrmztndad7e65hja52zk0c9x4yxgk0xsfuqk6s")
.unwrap();
let npub =
parse_npub("npub18s7md9ytv8r240jmag5j037huupk5jnsk94adykeaxtvc6lyftesuw5ydl").unwrap();

// create wallet
let mnemonic = generate_seed(12).unwrap();
let network = Network::Regtest;
let xpriv = ExtendedPrivKey::new_master(network, &mnemonic.to_seed("")).unwrap();
let storage = MemoryStorage::new(None, None, None);
let config = MutinyWalletConfigBuilder::new(xpriv)
.with_network(network)
.build();
let mut mw = MutinyWalletBuilder::new(xpriv, storage.clone()).with_config(config);
mw.with_nostr_key_source(NostrKeySource::Imported(nsec));
let mw = mw.build().await.expect("mutiny wallet should initialize");

// get messages
let limit = 5;
let messages = mw
.get_dm_conversation(None, npub, limit, None)
.await
.unwrap();

assert_eq!(messages.len(), 5);

for x in &messages {
log!("{}", x.message);
}

// get next messages
let limit = 2;
let util = messages.iter().min_by_key(|m| m.date).unwrap().date - 1;
let next = mw
.get_dm_conversation(None, npub, limit, Some(util))
.await
.unwrap();

for x in next.iter() {
log!("{}", x.message);
}

// check that we got different messages
assert_eq!(next.len(), 2);
assert!(next.iter().all(|m| !messages.contains(m)))
}
}
23 changes: 16 additions & 7 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1607,14 +1607,23 @@ impl MutinyWallet {
Ok(())
}

/// Decrypts a DM using the primary key
pub async fn decrypt_dm(
/// Get dm conversation between us and given npub
/// Returns a vector of messages sorted by newest first
pub async fn get_dm_conversation(
&self,
pubkey: String,
message: String,
) -> Result<String, MutinyJsError> {
let pubkey = parse_npub(&pubkey)?;
Ok(self.inner.nostr.decrypt_dm(pubkey, &message).await?)
primal_url: Option<String>,
npub: String,
limit: u64,
until: Option<u64>,
) -> Result<JsValue /* Vec<DirectMessage> */, MutinyJsError> {
let npub = parse_npub(&npub)?;
let vec = self
.inner
.get_dm_conversation(primal_url.as_deref(), npub, limit, until)
.await?;

let dms: Vec<DirectMessage> = vec.into_iter().map(|i| i.into()).collect();
Ok(JsValue::from_serde(&dms)?)
}

/// Resets the scorer and network graph. This can be useful if you get stuck in a bad state.
Expand Down
19 changes: 19 additions & 0 deletions mutiny-wasm/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,3 +1107,22 @@ impl TryFrom<nostr::nwc::BudgetPeriod> for BudgetPeriod {
}
}
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct DirectMessage {
pub from: String,
pub to: String,
pub message: String,
pub date: u64,
}

impl From<mutiny_core::DirectMessage> for DirectMessage {
fn from(value: mutiny_core::DirectMessage) -> Self {
Self {
from: value.from.to_bech32().expect("bech32"),
to: value.to.to_bech32().expect("bech32"),
message: value.message,
date: value.date,
}
}
}

0 comments on commit 5ffef58

Please sign in to comment.