mirror of
https://github.com/mikedilger/gossip.git
synced 2024-09-29 08:21:47 +00:00
Merge branch 'dmchat' into unstable
This commit is contained in:
commit
90913c7270
@ -1,3 +1,4 @@
|
||||
use crate::dm_channel::DmChannel;
|
||||
use nostr_types::{
|
||||
Event, Id, IdHex, Metadata, MilliSatoshi, PublicKey, RelayUrl, Tag, UncheckedUrl,
|
||||
};
|
||||
@ -28,7 +29,7 @@ pub enum ToOverlordMessage {
|
||||
MinionJobComplete(RelayUrl, u64),
|
||||
MinionJobUpdated(RelayUrl, u64, u64),
|
||||
PickRelays,
|
||||
Post(String, Vec<Tag>, Option<Id>),
|
||||
Post(String, Vec<Tag>, Option<Id>, Option<DmChannel>),
|
||||
PruneCache,
|
||||
PruneDatabase,
|
||||
PullFollow,
|
||||
|
@ -1,4 +1,5 @@
|
||||
use nostr_types::{PublicKey, Unixtime};
|
||||
use crate::globals::GLOBALS;
|
||||
use nostr_types::{Event, EventKind, PublicKey, Unixtime};
|
||||
use sha2::Digest;
|
||||
|
||||
/// This represents a DM (direct message) channel which includes a set
|
||||
@ -46,6 +47,57 @@ impl DmChannel {
|
||||
}
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
|
||||
pub fn from_event(event: &Event, my_pubkey: Option<PublicKey>) -> Option<DmChannel> {
|
||||
let my_pubkey = match my_pubkey {
|
||||
Some(pk) => pk,
|
||||
None => match GLOBALS.signer.public_key() {
|
||||
Some(pk) => pk,
|
||||
None => return None,
|
||||
},
|
||||
};
|
||||
|
||||
if event.kind == EventKind::EncryptedDirectMessage {
|
||||
let mut people: Vec<PublicKey> = event
|
||||
.people()
|
||||
.iter()
|
||||
.filter_map(|(pk, _, _)| PublicKey::try_from(pk).ok())
|
||||
.collect();
|
||||
people.push(event.pubkey);
|
||||
people.retain(|p| *p != my_pubkey);
|
||||
if people.len() > 1 {
|
||||
None
|
||||
} else {
|
||||
Some(Self::new(&people))
|
||||
}
|
||||
} else if event.kind == EventKind::GiftWrap {
|
||||
if let Ok(rumor) = GLOBALS.signer.unwrap_giftwrap(event) {
|
||||
let rumor_event = rumor.into_event_with_bad_signature();
|
||||
let mut people: Vec<PublicKey> = rumor_event
|
||||
.people()
|
||||
.iter()
|
||||
.filter_map(|(pk, _, _)| PublicKey::try_from(pk).ok())
|
||||
.collect();
|
||||
people.push(rumor_event.pubkey); // include author too
|
||||
people.retain(|p| *p != my_pubkey);
|
||||
Some(Self::new(&people))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if event.kind == EventKind::DmChat {
|
||||
// unwrapped rumor
|
||||
let mut people: Vec<PublicKey> = event
|
||||
.people()
|
||||
.iter()
|
||||
.filter_map(|(pk, _, _)| PublicKey::try_from(pk).ok())
|
||||
.collect();
|
||||
people.push(event.pubkey); // include author too
|
||||
people.retain(|p| *p != my_pubkey);
|
||||
Some(Self::new(&people))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
@ -8,6 +8,7 @@ pub enum ErrorKind {
|
||||
Empty(String),
|
||||
EventNotFound,
|
||||
General(String),
|
||||
GroupDmsNotYetSupported,
|
||||
HttpError(http::Error),
|
||||
JoinError(tokio::task::JoinError),
|
||||
Lmdb(heed::Error),
|
||||
@ -65,6 +66,7 @@ impl std::fmt::Display for Error {
|
||||
Delegation(s) => write!(f, "NIP-26 Delegation Error: {s}"),
|
||||
Empty(s) => write!(f, "{s} is empty"),
|
||||
EventNotFound => write!(f, "Event not found"),
|
||||
GroupDmsNotYetSupported => write!(f, "Group DMs are not yet supported"),
|
||||
General(s) => write!(f, "{s}"),
|
||||
HttpError(e) => write!(f, "HTTP error: {e}"),
|
||||
JoinError(e) => write!(f, "Task join error: {e}"),
|
||||
|
@ -4,6 +4,7 @@ use crate::comms::{
|
||||
RelayConnectionReason, RelayJob, ToMinionMessage, ToMinionPayload, ToMinionPayloadDetail,
|
||||
ToOverlordMessage,
|
||||
};
|
||||
use crate::dm_channel::DmChannel;
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::globals::{ZapState, GLOBALS};
|
||||
use crate::people::Person;
|
||||
@ -709,8 +710,8 @@ impl Overlord {
|
||||
count
|
||||
));
|
||||
}
|
||||
ToOverlordMessage::Post(content, tags, reply_to) => {
|
||||
self.post(content, tags, reply_to).await?;
|
||||
ToOverlordMessage::Post(content, tags, reply_to, dm_channel) => {
|
||||
self.post(content, tags, reply_to, dm_channel).await?;
|
||||
}
|
||||
ToOverlordMessage::PullFollow => {
|
||||
self.pull_following().await?;
|
||||
@ -935,11 +936,8 @@ impl Overlord {
|
||||
content: String,
|
||||
mut tags: Vec<Tag>,
|
||||
reply_to: Option<Id>,
|
||||
dm_channel: Option<DmChannel>,
|
||||
) -> Result<(), Error> {
|
||||
// We will fill this just before we create the event
|
||||
let mut tagged_pubkeys: Vec<PublicKey>;
|
||||
|
||||
let event = {
|
||||
let public_key = match GLOBALS.signer.public_key() {
|
||||
Some(pk) => pk,
|
||||
None => {
|
||||
@ -948,6 +946,23 @@ impl Overlord {
|
||||
}
|
||||
};
|
||||
|
||||
let pre_event = match dm_channel {
|
||||
Some(dmc) => {
|
||||
if dmc.keys().len() > 1 {
|
||||
return Err((ErrorKind::GroupDmsNotYetSupported, file!(), line!()).into());
|
||||
}
|
||||
|
||||
let recipient = if dmc.keys().is_empty() {
|
||||
public_key // must be to yourself
|
||||
} else {
|
||||
dmc.keys()[0]
|
||||
};
|
||||
|
||||
// On a DM, we ignore tags and reply_to
|
||||
|
||||
GLOBALS.signer.new_nip04(recipient, &content)?
|
||||
}
|
||||
_ => {
|
||||
if GLOBALS.storage.read_setting_set_client_tag() {
|
||||
tags.push(Tag::Other {
|
||||
tag: "client".to_owned(),
|
||||
@ -983,11 +998,15 @@ impl Overlord {
|
||||
add_event_to_tags(&mut tags, *id, "mention").await;
|
||||
}
|
||||
NostrBech32::Profile(prof) => {
|
||||
if dm_channel.is_none() {
|
||||
add_pubkey_to_tags(&mut tags, &prof.pubkey).await;
|
||||
}
|
||||
}
|
||||
NostrBech32::Pubkey(pk) => {
|
||||
if dm_channel.is_none() {
|
||||
add_pubkey_to_tags(&mut tags, pk).await;
|
||||
}
|
||||
}
|
||||
NostrBech32::Relay(_) => {
|
||||
// we don't need to add this to tags I don't think.
|
||||
}
|
||||
@ -1016,11 +1035,14 @@ impl Overlord {
|
||||
|
||||
// Add a 'p' tag for the author we are replying to (except if it is our own key)
|
||||
if parent.pubkey != public_key {
|
||||
if dm_channel.is_none() {
|
||||
add_pubkey_to_tags(&mut tags, &parent.pubkey).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Add all the 'p' tags from the note we are replying to (except our own)
|
||||
// FIXME: Should we avoid taging people who are muted?
|
||||
if dm_channel.is_none() {
|
||||
for tag in &parent.tags {
|
||||
if let Tag::Pubkey { pubkey, .. } = tag {
|
||||
if pubkey.as_str() != public_key.as_hex_string() {
|
||||
@ -1028,6 +1050,7 @@ impl Overlord {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((root, _maybeurl)) = parent.replies_to_root() {
|
||||
// Add an 'e' tag for the root
|
||||
@ -1060,8 +1083,20 @@ impl Overlord {
|
||||
}
|
||||
}
|
||||
|
||||
PreEvent {
|
||||
pubkey: public_key,
|
||||
created_at: Unixtime::now().unwrap(),
|
||||
kind: EventKind::TextNote,
|
||||
tags,
|
||||
content,
|
||||
ots: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Copy the tagged pubkeys for determine which relays to send to
|
||||
tagged_pubkeys = tags
|
||||
let mut tagged_pubkeys: Vec<PublicKey> = pre_event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|t| {
|
||||
if let Tag::Pubkey { pubkey, .. } = t {
|
||||
@ -1075,15 +1110,7 @@ impl Overlord {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let pre_event = PreEvent {
|
||||
pubkey: public_key,
|
||||
created_at: Unixtime::now().unwrap(),
|
||||
kind: EventKind::TextNote,
|
||||
tags,
|
||||
content,
|
||||
ots: None,
|
||||
};
|
||||
|
||||
let event = {
|
||||
let powint = GLOBALS.storage.read_setting_pow();
|
||||
let pow = if powint > 0 { Some(powint) } else { None };
|
||||
let (work_sender, work_receiver) = mpsc::channel();
|
||||
|
@ -197,6 +197,17 @@ impl Signer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_nip04(
|
||||
&self,
|
||||
recipient_public_key: PublicKey,
|
||||
message: &str,
|
||||
) -> Result<PreEvent, Error> {
|
||||
match &*self.private.read() {
|
||||
Some(pk) => Ok(PreEvent::new_nip04(pk, recipient_public_key, message)?),
|
||||
_ => Err((ErrorKind::NoPrivateKey, file!(), line!()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn export_private_key_bech32(&self, pass: &str) -> Result<String, Error> {
|
||||
let maybe_encrypted = self.encrypted.read().to_owned();
|
||||
match maybe_encrypted {
|
||||
|
@ -2324,27 +2324,9 @@ impl Storage {
|
||||
let unread = 1 - self.is_event_viewed(event.id)? as usize;
|
||||
if event.kind == EventKind::EncryptedDirectMessage {
|
||||
let time = event.created_at;
|
||||
let dmchannel = {
|
||||
if event.pubkey != my_pubkey {
|
||||
// DM sent to me
|
||||
DmChannel::new(&[event.pubkey])
|
||||
} else {
|
||||
// DM sent from me
|
||||
let mut maybe_channel: Option<DmChannel> = None;
|
||||
for tag in event.tags.iter() {
|
||||
if let Tag::Pubkey { pubkey, .. } = tag {
|
||||
if let Ok(pk) = PublicKey::try_from(pubkey) {
|
||||
if pk != my_pubkey {
|
||||
maybe_channel = Some(DmChannel::new(&[pk]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match maybe_channel {
|
||||
Some(dmchannel) => dmchannel,
|
||||
let dmchannel = match DmChannel::from_event(event, Some(my_pubkey)) {
|
||||
Some(dmc) => dmc,
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
};
|
||||
map.entry(dmchannel.clone())
|
||||
.and_modify(|d| {
|
||||
@ -2362,15 +2344,9 @@ impl Storage {
|
||||
if let Ok(rumor) = GLOBALS.signer.unwrap_giftwrap(event) {
|
||||
let rumor_event = rumor.into_event_with_bad_signature();
|
||||
let time = rumor_event.created_at;
|
||||
let dmchannel = {
|
||||
let mut people: Vec<PublicKey> = rumor_event
|
||||
.people()
|
||||
.iter()
|
||||
.filter_map(|(pk, _, _)| PublicKey::try_from(pk).ok())
|
||||
.filter(|pk| *pk != my_pubkey)
|
||||
.collect();
|
||||
people.push(rumor_event.pubkey); // include author too
|
||||
DmChannel::new(&people)
|
||||
let dmchannel = match DmChannel::from_event(&rumor_event, Some(my_pubkey)) {
|
||||
Some(dmc) => dmc,
|
||||
None => continue,
|
||||
};
|
||||
map.entry(dmchannel.clone())
|
||||
.and_modify(|d| {
|
||||
@ -2400,61 +2376,25 @@ impl Storage {
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
let mut pass1 = self.find_events(
|
||||
let mut output: Vec<Event> = self.find_events(
|
||||
&[EventKind::EncryptedDirectMessage, EventKind::GiftWrap],
|
||||
&[],
|
||||
Some(Unixtime(0)),
|
||||
|event| {
|
||||
if event.kind == EventKind::EncryptedDirectMessage {
|
||||
if channel.keys().len() > 1 { return false; }
|
||||
if channel.keys().len() == 0 { return true; } // self-channel
|
||||
let other = &channel.keys()[0];
|
||||
let people = event.people();
|
||||
match people.len() {
|
||||
1 => (event.pubkey == my_pubkey && event.is_tagged(other))
|
||||
|| (event.pubkey == *other && event.is_tagged(&my_pubkey)),
|
||||
2 => (event.pubkey == my_pubkey || event.pubkey == *other)
|
||||
&& (event.is_tagged(&my_pubkey) && event.is_tagged(other)),
|
||||
_ => false,
|
||||
if let Some(event_dm_channel) = DmChannel::from_event(event, Some(my_pubkey)) {
|
||||
if event_dm_channel == *channel {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if event.kind == EventKind::GiftWrap {
|
||||
// Decrypt in next pass, else we would have to decrypt twice
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
false,
|
||||
)?;
|
||||
|
||||
let mut pass2: Vec<Event> = Vec::new();
|
||||
|
||||
for event in pass1.drain(..) {
|
||||
if event.kind == EventKind::EncryptedDirectMessage {
|
||||
pass2.push(event); // already validated
|
||||
} else if event.kind == EventKind::GiftWrap {
|
||||
if let Ok(rumor) = GLOBALS.signer.unwrap_giftwrap(&event) {
|
||||
let mut rumor_event = rumor.into_event_with_bad_signature();
|
||||
rumor_event.id = event.id; // lie, so it indexes it under the giftwrap
|
||||
let mut tagged: Vec<PublicKey> = rumor_event
|
||||
.people()
|
||||
.drain(..)
|
||||
.filter_map(|(pkh, _, _)| PublicKey::try_from(pkh).ok())
|
||||
.collect();
|
||||
tagged.push(rumor_event.pubkey); // include author
|
||||
tagged.retain(|pk| *pk != my_pubkey); // never include user
|
||||
let this_channel = DmChannel::new(&tagged);
|
||||
if this_channel == *channel {
|
||||
pass2.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort
|
||||
pass2.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
output.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
Ok(pass2.iter().map(|e| e.id).collect())
|
||||
Ok(output.iter().map(|e| e.id).collect())
|
||||
}
|
||||
|
||||
pub fn rebuild_event_indices(&self) -> Result<(), Error> {
|
||||
|
@ -16,7 +16,6 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
|
||||
|
||||
ui.heading("Direct Private Message Channels");
|
||||
ui.add_space(12.0);
|
||||
ui.separator();
|
||||
|
||||
ScrollArea::vertical()
|
||||
.id_source("dm_chat_list")
|
||||
|
@ -8,6 +8,7 @@ use super::notedata::{NoteData, RepostType};
|
||||
|
||||
use super::FeedNoteParams;
|
||||
use crate::comms::ToOverlordMessage;
|
||||
use crate::dm_channel::DmChannel;
|
||||
use crate::feed::FeedKind;
|
||||
use crate::globals::{ZapState, GLOBALS};
|
||||
use crate::ui::widgets::CopyButton;
|
||||
@ -615,8 +616,9 @@ fn render_note_inner(
|
||||
|
||||
ui.add_space(24.0);
|
||||
|
||||
if render_data.can_post
|
||||
&& note.event.kind != EventKind::EncryptedDirectMessage
|
||||
if render_data.can_post {
|
||||
if note.event.kind != EventKind::EncryptedDirectMessage
|
||||
&& note.event.kind != EventKind::DmChat
|
||||
{
|
||||
// Button to Repost
|
||||
if ui
|
||||
@ -629,6 +631,7 @@ fn render_note_inner(
|
||||
{
|
||||
app.draft_repost = Some(note.event.id);
|
||||
app.replying_to = None;
|
||||
app.draft_dm_channel = None;
|
||||
app.show_post_area = true;
|
||||
}
|
||||
|
||||
@ -665,11 +668,13 @@ fn render_note_inner(
|
||||
app.draft.push_str(&format!("{}", nostr_url));
|
||||
app.draft_repost = None;
|
||||
app.replying_to = None;
|
||||
app.draft_dm_channel = None;
|
||||
app.show_post_area = true;
|
||||
app.draft_needs_focus = true;
|
||||
}
|
||||
|
||||
ui.add_space(24.0);
|
||||
}
|
||||
|
||||
// Button to reply
|
||||
if ui
|
||||
@ -683,6 +688,8 @@ fn render_note_inner(
|
||||
app.replying_to = Some(note.event.id);
|
||||
app.draft_repost = None;
|
||||
app.show_post_area = true;
|
||||
app.draft_dm_channel =
|
||||
DmChannel::from_event(¬e.event, None);
|
||||
app.draft_needs_focus = true;
|
||||
}
|
||||
|
||||
|
@ -163,6 +163,12 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(dm_channel) = &app.draft_dm_channel {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("DIRECT MESSAGE TO: {}", dm_channel.name()));
|
||||
});
|
||||
}
|
||||
|
||||
let draft_response = ui.add(
|
||||
text_edit_multiline!(app, app.draft)
|
||||
.id_source("compose_area")
|
||||
@ -296,6 +302,7 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
|
||||
app.draft.clone(),
|
||||
tags,
|
||||
Some(replying_to_id),
|
||||
app.draft_dm_channel.clone(),
|
||||
));
|
||||
}
|
||||
None => {
|
||||
@ -308,6 +315,7 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
|
||||
app.draft.clone(),
|
||||
tags,
|
||||
None,
|
||||
app.draft_dm_channel.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ mod you;
|
||||
|
||||
use crate::about::About;
|
||||
use crate::comms::ToOverlordMessage;
|
||||
use crate::dm_channel::DmChannel;
|
||||
use crate::error::Error;
|
||||
use crate::feed::FeedKind;
|
||||
use crate::globals::{ZapState, GLOBALS};
|
||||
@ -243,6 +244,7 @@ struct GossipUi {
|
||||
subject: String,
|
||||
include_content_warning: bool,
|
||||
content_warning: String,
|
||||
draft_dm_channel: Option<DmChannel>,
|
||||
replying_to: Option<Id>,
|
||||
|
||||
// User entry: metadata
|
||||
@ -460,6 +462,7 @@ impl GossipUi {
|
||||
subject: "".to_owned(),
|
||||
include_content_warning: false,
|
||||
content_warning: "".to_owned(),
|
||||
draft_dm_channel: None,
|
||||
replying_to: None,
|
||||
editing_metadata: false,
|
||||
metadata: Metadata::new(),
|
||||
@ -551,6 +554,7 @@ impl GossipUi {
|
||||
self.include_subject = false;
|
||||
self.subject = "".to_owned();
|
||||
self.replying_to = None;
|
||||
self.draft_dm_channel = None;
|
||||
self.include_content_warning = false;
|
||||
self.content_warning = "".to_owned();
|
||||
}
|
||||
@ -802,6 +806,11 @@ impl eframe::App for GossipUi {
|
||||
let response = ui.add_sized([crate::AVATAR_SIZE_F32, crate::AVATAR_SIZE_F32], egui::Button::new(text.color(self.settings.theme.navigation_text_color())).stroke(egui::Stroke::NONE).rounding(egui::Rounding::same(crate::AVATAR_SIZE_F32)).fill(self.settings.theme.navigation_bg_fill()));
|
||||
if response.clicked() {
|
||||
self.show_post_area = true;
|
||||
if let Page::Feed(FeedKind::DmChat(channel)) = &self.page {
|
||||
self.draft_dm_channel = Some(channel.clone());
|
||||
} else {
|
||||
self.draft_dm_channel = None;
|
||||
}
|
||||
if GLOBALS.signer.is_ready() {
|
||||
self.draft_needs_focus = true;
|
||||
} else {
|
||||
@ -947,6 +956,15 @@ impl GossipUi {
|
||||
app.set_page(Page::Feed(FeedKind::Person(person.pubkey)));
|
||||
}
|
||||
}
|
||||
if GLOBALS.signer.is_ready() {
|
||||
if ui.button("Send DM").clicked() {
|
||||
app.replying_to = None;
|
||||
app.draft_repost = None;
|
||||
app.show_post_area = true;
|
||||
app.draft_dm_channel = Some(DmChannel::new(&[person.pubkey]));
|
||||
app.draft_needs_focus = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if person.followed {
|
||||
|
@ -73,12 +73,14 @@ macro_rules! theme_dispatch {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn highlight_color(&self) -> Color32 {
|
||||
match self.variant {
|
||||
$( $variant => $class::highlight_color(self.dark_mode), )+
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn accent_complementary_color(&self) -> Color32 {
|
||||
match self.variant {
|
||||
$( $variant => $class::accent_complementary_color(self.dark_mode), )+
|
||||
|
Loading…
Reference in New Issue
Block a user