Merge remote-tracking branch 'dilger/unstable' into feature/warn-no-audio-device

This commit is contained in:
Bu5hm4nn 2024-03-21 11:21:17 -06:00
commit 21101d8f41
30 changed files with 819 additions and 285 deletions

View File

@ -16,6 +16,7 @@ video-ffmpeg = [ "egui-video", "sdl2" ]
native-tls = [ "gossip-lib/native-tls" ]
rustls-tls = [ "gossip-lib/rustls-tls" ]
rustls-tls-native = [ "gossip-lib/rustls-tls-native" ]
appimage = [ "gossip-lib/appimage" ]
[dependencies]
bech32 = "0.9"

View File

@ -29,7 +29,7 @@ impl Command {
}
}
const COMMANDS: [Command; 30] = [
const COMMANDS: [Command; 31] = [
Command {
cmd: "oneshot",
usage_params: "{depends}",
@ -45,6 +45,11 @@ const COMMANDS: [Command; 30] = [
usage_params: "<listname>",
desc: "add a new person list with the given name",
},
Command {
cmd: "backdate_eose",
usage_params: "",
desc: "backdate last_general_eose_at by 24 hours for every relay",
},
Command {
cmd: "bech32_decode",
usage_params: "<bech32string>",
@ -202,6 +207,7 @@ pub fn handle_command(mut args: env::Args, runtime: &Runtime) -> Result<bool, Er
"oneshot" => oneshot(command, args)?,
"add_person_relay" => add_person_relay(command, args)?,
"add_person_list" => add_person_list(command, args)?,
"backdate_eose" => backdate_eose()?,
"bech32_decode" => bech32_decode(command, args)?,
"bech32_encode_event_addr" => bech32_encode_event_addr(command, args)?,
"decrypt" => decrypt(command, args)?,
@ -307,6 +313,24 @@ pub fn add_person_list(cmd: Command, mut args: env::Args) -> Result<(), Error> {
Ok(())
}
pub fn backdate_eose() -> Result<(), Error> {
let now = Unixtime::now().unwrap();
let ago = (now.0 - 60 * 60 * 24) as u64;
GLOBALS.storage.modify_all_relays(
|relay| {
if let Some(eose) = relay.last_general_eose_at {
if eose > ago {
relay.last_general_eose_at = Some(ago);
}
}
},
None,
)?;
Ok(())
}
pub fn bech32_decode(cmd: Command, mut args: env::Args) -> Result<(), Error> {
let mut param = match args.next() {
Some(s) => s,

View File

@ -256,7 +256,10 @@ fn render_a_feed(
is_thread: threaded,
};
let feed_newest_at_bottom = GLOBALS.storage.read_setting_feed_newest_at_bottom();
app.vert_scroll_area()
.stick_to_bottom(feed_newest_at_bottom)
.id_source(scroll_area_id)
.show(ui, |ui| {
egui::Frame::none()
@ -264,66 +267,82 @@ fn render_a_feed(
.fill(app.theme.feed_scroll_fill(&feed_properties))
.stroke(app.theme.feed_scroll_stroke(&feed_properties))
.show(ui, |ui| {
let iter = feed.iter();
let first = feed.first();
let last = feed.last();
for id in iter {
render_note_maybe_fake(
app,
ctx,
ui,
FeedNoteParams {
id: *id,
indent: 0,
as_reply_to: false,
threaded,
is_first: Some(id) == first,
is_last: Some(id) == last,
},
);
}
let recomputing = GLOBALS
.feed
.recompute_lock
.load(std::sync::atomic::Ordering::Relaxed);
if !recomputing && offer_load_more {
if feed_newest_at_bottom {
ui.add_space(50.0);
if offer_load_more {
render_load_more(app, ui)
}
ui.add_space(50.0);
ui.with_layout(
egui::Layout::top_down(egui::Align::Center)
.with_cross_align(egui::Align::Center),
|ui| {
app.theme.accent_button_1_style(ui.style_mut());
ui.spacing_mut().button_padding.x *= 3.0;
ui.spacing_mut().button_padding.y *= 2.0;
let response = ui.add(egui::Button::new("Load More"));
if response.clicked() {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::LoadMoreCurrentFeed);
}
// draw some nice lines left and right of the button
let stroke = egui::Stroke::new(1.5, ui.visuals().extreme_bg_color);
let width =
(ui.available_width() - response.rect.width()) / 2.0 - 20.0;
let left_start =
response.rect.left_center() - egui::vec2(10.0, 0.0);
let left_end = left_start - egui::vec2(width, 0.0);
ui.painter().line_segment([left_start, left_end], stroke);
let right_start =
response.rect.right_center() + egui::vec2(10.0, 0.0);
let right_end = right_start + egui::vec2(width, 0.0);
ui.painter().line_segment([right_start, right_end], stroke);
},
);
for id in feed.iter().rev() {
render_note_maybe_fake(
app,
ctx,
ui,
FeedNoteParams {
id: *id,
indent: 0,
as_reply_to: false,
threaded,
is_first: Some(id) == feed.last(),
is_last: Some(id) == feed.first(),
},
);
}
} else {
for id in feed.iter() {
render_note_maybe_fake(
app,
ctx,
ui,
FeedNoteParams {
id: *id,
indent: 0,
as_reply_to: false,
threaded,
is_first: Some(id) == feed.first(),
is_last: Some(id) == feed.last(),
},
);
}
ui.add_space(50.0);
if offer_load_more {
render_load_more(app, ui)
}
ui.add_space(50.0);
}
ui.add_space(100.0);
});
});
}
fn render_load_more(app: &mut GossipUi, ui: &mut Ui) {
ui.with_layout(
egui::Layout::top_down(egui::Align::Center).with_cross_align(egui::Align::Center),
|ui| {
app.theme.accent_button_1_style(ui.style_mut());
ui.spacing_mut().button_padding.x *= 3.0;
ui.spacing_mut().button_padding.y *= 2.0;
let response = ui.add(egui::Button::new("Load More"));
if response.clicked() {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::LoadMoreCurrentFeed);
}
// draw some nice lines left and right of the button
let stroke = egui::Stroke::new(1.5, ui.visuals().extreme_bg_color);
let width = (ui.available_width() - response.rect.width()) / 2.0 - 20.0;
let left_start = response.rect.left_center() - egui::vec2(10.0, 0.0);
let left_end = left_start - egui::vec2(width, 0.0);
ui.painter().line_segment([left_start, left_end], stroke);
let right_start = response.rect.right_center() + egui::vec2(10.0, 0.0);
let right_end = right_start + egui::vec2(width, 0.0);
ui.painter().line_segment([right_start, right_end], stroke);
},
);
}
fn render_note_maybe_fake(
app: &mut GossipUi,
ctx: &Context,

View File

@ -153,11 +153,14 @@ pub(super) fn render_note(
// scroll to this note if it's the main note of a thread and the user hasn't scrolled yet
if is_main_event && app.feeds.thread_needs_scroll {
// keep auto-scrolling untill user scrolls
// keep auto-scrolling until user scrolls
if app.current_scroll_offset != 0.0 {
app.feeds.thread_needs_scroll = false;
}
inner_response.response.scroll_to_me(Some(Align::Center));
// only request scrolling if the note is not completely visible
if !ui.clip_rect().contains_rect(inner_response.response.rect) {
inner_response.response.scroll_to_me(Some(Align::Center));
}
}
// Mark post as viewed if hovered AND we are not scrolling

View File

@ -629,6 +629,7 @@ impl GossipUi {
let mut wizard_state: WizardState = Default::default();
let wizard_complete = GLOBALS.storage.get_flag_wizard_complete();
if !wizard_complete {
wizard_state.init();
if let Some(wp) = wizard::start_wizard_page(&mut wizard_state) {
start_page = Page::Wizard(wp);
}
@ -1172,9 +1173,20 @@ impl GossipUi {
// ---- "plus icon" ----
if !self.show_post_area_fn() && self.page.show_post_icon() {
let bottom_right = ui.ctx().screen_rect().right_bottom();
let pos = bottom_right
+ Vec2::new(-crate::AVATAR_SIZE_F32 * 2.0, -crate::AVATAR_SIZE_F32 * 2.0);
let feed_newest_at_bottom =
GLOBALS.storage.read_setting_feed_newest_at_bottom();
let pos = if feed_newest_at_bottom {
let top_right = ui.ctx().screen_rect().right_top();
top_right
+ Vec2::new(-crate::AVATAR_SIZE_F32 * 2.0, crate::AVATAR_SIZE_F32 * 2.0)
} else {
let bottom_right = ui.ctx().screen_rect().right_bottom();
bottom_right
+ Vec2::new(
-crate::AVATAR_SIZE_F32 * 2.0,
-crate::AVATAR_SIZE_F32 * 2.0,
)
};
egui::Area::new(ui.next_auto_id())
.movable(false)
@ -1194,6 +1206,15 @@ impl GossipUi {
} else {
RichText::new("\u{1f513}").size(20.0)
};
let fill_color = {
let fill_color_tuple = self.theme.accent_color().to_tuple();
Color32::from_rgba_premultiplied(
fill_color_tuple.0,
fill_color_tuple.1,
fill_color_tuple.2,
128, // half transparent
)
};
let response = ui.add_sized(
[crate::AVATAR_SIZE_F32, crate::AVATAR_SIZE_F32],
egui::Button::new(
@ -1201,7 +1222,7 @@ impl GossipUi {
)
.stroke(egui::Stroke::NONE)
.rounding(egui::Rounding::same(crate::AVATAR_SIZE_F32))
.fill(self.theme.accent_color()),
.fill(fill_color),
);
if response.clicked() {
self.show_post_area = true;
@ -1592,21 +1613,14 @@ impl GossipUi {
}
}
if !followed && ui.button("Follow").clicked() {
let _ = GLOBALS.people.follow(
&person.pubkey,
true,
PersonList::Followed,
true,
true,
);
let _ = GLOBALS
.people
.follow(&person.pubkey, true, PersonList::Followed, true);
} else if followed && ui.button("Unfollow").clicked() {
let _ = GLOBALS.people.follow(
&person.pubkey,
false,
PersonList::Followed,
true,
false,
);
let _ =
GLOBALS
.people
.follow(&person.pubkey, false, PersonList::Followed, true);
}
// Do not show 'Mute' if this is yourself

View File

@ -3,7 +3,7 @@ use std::time::{Duration, Instant};
use super::{GossipUi, Page};
use crate::ui::widgets;
use crate::AVATAR_SIZE_F32;
use eframe::egui;
use eframe::egui::{self, Label, Sense};
use egui::{Context, RichText, Ui, Vec2};
use egui_winit::egui::text::LayoutJob;
use egui_winit::egui::text_edit::TextEditOutput;
@ -27,6 +27,7 @@ pub(in crate::ui) struct ListUi {
add_contact_searched: Option<String>,
add_contact_search_results: Vec<(String, PublicKey)>,
add_contact_search_selected: Option<usize>,
add_contact_error: Option<String>,
entering_follow_someone_on_list: bool,
clear_list_needs_confirm: bool,
@ -49,6 +50,7 @@ impl ListUi {
add_contact_searched: None,
add_contact_search_results: Vec::new(),
add_contact_search_selected: None,
add_contact_error: None,
entering_follow_someone_on_list: false,
clear_list_needs_confirm: false,
@ -218,7 +220,7 @@ pub(super) fn update(
app.placeholder_avatar.clone()
};
let avatar_response =
let mut response =
widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed);
ui.add_space(20.0);
@ -226,7 +228,11 @@ pub(super) fn update(
ui.vertical(|ui| {
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label(RichText::new(person.best_name()).size(15.5));
response |= ui.add(
Label::new(RichText::new(person.best_name()).size(15.5))
.selectable(false)
.sense(Sense::click()),
);
ui.add_space(10.0);
@ -235,14 +241,22 @@ pub(super) fn update(
.have_persons_relays(person.pubkey)
.unwrap_or(false)
{
ui.label(
RichText::new("Relay list not found")
.color(app.theme.warning_marker_text_color()),
response |= ui.add(
Label::new(
RichText::new("Relay list not found")
.color(app.theme.warning_marker_text_color()),
)
.selectable(false)
.sense(Sense::click()),
);
}
});
ui.add_space(3.0);
ui.label(GossipUi::richtext_from_person_nip05(person).weak());
response |= ui.add(
Label::new(GossipUi::richtext_from_person_nip05(person).weak())
.selectable(false)
.sense(Sense::click()),
);
});
ui.vertical(|ui| {
@ -266,7 +280,7 @@ pub(super) fn update(
if list != PersonList::Followed {
// private / public switch
ui.label("Private");
ui.add(Label::new("Private").selectable(false));
if ui
.add(widgets::Switch::onoff(&app.theme, &mut private))
.clicked()
@ -282,17 +296,21 @@ pub(super) fn update(
}
},
);
});
if avatar_response.clicked() {
app.set_page(ctx, Page::Person(person.pubkey));
}
});
response
})
.inner
})
.inner
},
);
if row_response
.response
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
|| row_response
.inner
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
app.set_page(ctx, Page::Person(person.pubkey));
}
@ -350,7 +368,7 @@ fn render_add_contact_popup(
list: PersonList,
metadata: &PersonListMetadata,
) {
const DLG_SIZE: Vec2 = vec2(400.0, 240.0);
const DLG_SIZE: Vec2 = vec2(400.0, 260.0);
let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, DLG_SIZE, true, |ui| {
let enter_key;
(app.people_list.add_contact_search_selected, enter_key) =
@ -367,6 +385,18 @@ fn render_add_contact_popup(
ui.heading("Add contact to the list");
ui.add_space(8.0);
// error block
ui.label(
RichText::new(
app.people_list
.add_contact_error
.as_ref()
.unwrap_or(&"".to_string()),
)
.color(app.theme.warning_marker_text_color()),
);
ui.add_space(8.0);
ui.label("Search for known contacts to add");
ui.add_space(8.0);
@ -466,10 +496,7 @@ fn render_add_contact_popup(
mark_refresh(app);
} else {
add_failed = true;
GLOBALS
.status_queue
.write()
.write("Invalid pubkey.".to_string());
app.people_list.add_contact_error = Some("Invalid pubkey.".to_string());
}
if !add_failed {
app.add_contact = "".to_owned();
@ -684,6 +711,11 @@ pub(super) fn render_more_list_actions(
count: usize,
on_list: bool,
) {
// do not show for "Following" and "Muted"
if !on_list && !matches!(list, PersonList::Custom(_)) {
return;
}
if on_list {
app.theme.accent_button_1_style(ui.style_mut());
}
@ -697,12 +729,6 @@ pub(super) fn render_more_list_actions(
app.theme.accent_button_1_style(ui.style_mut());
ui.spacing_mut().item_spacing.y = 15.0;
}
if !on_list {
if ui.button("View Contacts").clicked() {
app.set_page(ui.ctx(), Page::PeopleList(list));
*is_open = false;
}
}
if matches!(list, PersonList::Custom(_)) {
if ui.button("Rename").clicked() {
app.deleting_list = None;

View File

@ -2,7 +2,7 @@ use std::cmp::Ordering;
use super::{GossipUi, Page};
use crate::ui::widgets;
use eframe::egui;
use eframe::egui::{self, Sense};
use egui::{Context, Ui};
use egui_winit::egui::{Label, RichText};
use gossip_lib::{PersonList, PersonListMetadata, GLOBALS};
@ -55,22 +55,39 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.add(Label::new(
RichText::new(&metadata.title).heading().color(color),
));
ui.label(format!("({})", metadata.len));
let mut response = ui.add(
Label::new(
RichText::new(&metadata.title).heading().color(color),
)
.selectable(false)
.sense(egui::Sense::click()),
);
response |= ui.add(
Label::new(format!("({})", metadata.len))
.selectable(false)
.sense(Sense::click()),
);
if metadata.favorite {
ui.add(Label::new(
RichText::new("")
.size(18.0)
.color(app.theme.accent_complementary_color()),
));
response |= ui.add(
Label::new(
RichText::new("")
.size(18.0)
.color(app.theme.accent_complementary_color()),
)
.selectable(false)
.sense(Sense::click()),
);
}
if metadata.private {
ui.add(Label::new(
RichText::new("😎")
.color(app.theme.accent_complementary_color()),
));
response |= ui.add(
Label::new(
RichText::new("😎")
.color(app.theme.accent_complementary_color()),
)
.selectable(false)
.sense(Sense::click()),
);
}
ui.with_layout(
@ -87,14 +104,21 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
);
},
);
});
});
response
})
.inner
})
.inner
},
);
if row_response
.response
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
|| row_response
.inner
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
app.set_page(ctx, Page::PeopleList(list));
}

View File

@ -15,6 +15,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
&mut app.unsaved_settings.posting_area_at_top,
"Show posting area at the top instead of the bottom",
);
ui.checkbox(
&mut app.unsaved_settings.feed_newest_at_bottom,
"Order feed with newest at bottom (intead of top)",
);
ui.add_space(20.0);
ui.horizontal(|ui| {

View File

@ -14,7 +14,7 @@ use std::collections::BTreeMap;
const MIN_OUTBOX: usize = 3;
const MIN_INBOX: usize = 2;
const MIN_DISCOVERY: usize = 1;
const MIN_DISCOVERY: usize = 4;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let read_relay = |url: &RelayUrl| GLOBALS.storage.read_or_create_relay(url, None).unwrap();
@ -135,7 +135,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.label(RichText::new(" - OK").color(Color32::GREEN));
} else {
ui.label(
RichText::new(" - You should have one")
RichText::new(" - We suggest 4")
.color(app.theme.warning_marker_text_color()),
);
}

View File

@ -76,6 +76,13 @@ impl Default for WizardState {
}
}
impl WizardState {
pub fn init(&mut self) {
if self.need_discovery_relays() {
let purplepages = RelayUrl::try_from_str("wss://purplepag.es/").unwrap();
super::modify_relay(&purplepages, |relay| relay.set_usage_bits(Relay::DISCOVER));
}
}
pub fn update(&mut self) {
self.follow_only = GLOBALS.storage.get_flag_following_only();
@ -112,11 +119,6 @@ impl WizardState {
.map(|(pk, _)| (Some(pk), None))
.collect();
if self.need_discovery_relays() {
let purplepages = RelayUrl::try_from_str("wss://purplepag.es/").unwrap();
super::modify_relay(&purplepages, |relay| relay.set_usage_bits(Relay::DISCOVER));
}
// Copy any new status queue messages into our local error variable
let last_status_queue_message = GLOBALS.status_queue.read().read_last();
if last_status_queue_message != self.last_status_queue_message {

View File

@ -89,6 +89,7 @@ pub struct UnsavedSettings {
pub follow_os_dark_mode: bool,
pub override_dpi: Option<u32>,
pub highlight_unread_events: bool,
pub feed_newest_at_bottom: bool,
pub posting_area_at_top: bool,
pub status_bar: bool,
pub image_resize_algorithm: String,
@ -172,6 +173,7 @@ impl Default for UnsavedSettings {
follow_os_dark_mode: default_setting!(follow_os_dark_mode),
override_dpi: default_setting!(override_dpi),
highlight_unread_events: default_setting!(highlight_unread_events),
feed_newest_at_bottom: default_setting!(feed_newest_at_bottom),
posting_area_at_top: default_setting!(posting_area_at_top),
status_bar: default_setting!(status_bar),
image_resize_algorithm: default_setting!(image_resize_algorithm),
@ -257,6 +259,7 @@ impl UnsavedSettings {
follow_os_dark_mode: load_setting!(follow_os_dark_mode),
override_dpi: load_setting!(override_dpi),
highlight_unread_events: load_setting!(highlight_unread_events),
feed_newest_at_bottom: load_setting!(feed_newest_at_bottom),
posting_area_at_top: load_setting!(posting_area_at_top),
status_bar: load_setting!(status_bar),
image_resize_algorithm: load_setting!(image_resize_algorithm),
@ -338,6 +341,7 @@ impl UnsavedSettings {
save_setting!(follow_os_dark_mode, self, txn);
save_setting!(override_dpi, self, txn);
save_setting!(highlight_unread_events, self, txn);
save_setting!(feed_newest_at_bottom, self, txn);
save_setting!(posting_area_at_top, self, txn);
save_setting!(status_bar, self, txn);
save_setting!(image_resize_algorithm, self, txn);

View File

@ -35,6 +35,9 @@ rustls-tls-native = [
"tokio-tungstenite/rustls-tls-native-roots"
]
# Make tweaks for AppImage
appimage = []
[dependencies]
async-recursion = "1.0"
async-trait = "0.1"

View File

@ -143,7 +143,7 @@ pub enum ToOverlordMessage {
RankRelay(RelayUrl, u8),
/// internal (the overlord sends messages to itself sometimes!)
ReengageMinion(RelayUrl, Vec<RelayJob>),
ReengageMinion(RelayUrl),
/// Calls [refresh_scores_and_pick_relays](crate::Overlord::refresh_scores_and_pick_relays)
RefreshScoresAndPickRelays,
@ -235,7 +235,7 @@ pub(crate) struct ToMinionPayload {
pub detail: ToMinionPayloadDetail,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ToMinionPayloadDetail {
AdvertiseRelayList(Box<Event>),
AuthApproved,
@ -354,3 +354,10 @@ pub struct RelayJob {
// overlord.minions_task_url
// GLOBALS.relay_picker
}
impl RelayJob {
// This is like equality, but ignores the random job id
pub fn matches(&self, other: &RelayJob) -> bool {
self.reason == other.reason && self.payload.detail == other.payload.detail
}
}

View File

@ -5,19 +5,19 @@ use crate::feed::Feed;
use crate::fetcher::Fetcher;
use crate::gossip_identity::GossipIdentity;
use crate::media::Media;
use crate::misc::ZapState;
use crate::nip46::ParsedCommand;
use crate::pending::Pending;
use crate::people::{People, Person};
use crate::relay::Relay;
use crate::relay_picker_hooks::Hooks;
use crate::seeker::Seeker;
use crate::status::StatusQueue;
use crate::storage::Storage;
use crate::RunState;
use dashmap::{DashMap, DashSet};
use gossip_relay_picker::RelayPicker;
use nostr_types::{
Event, Id, PayRequestData, Profile, PublicKey, RelayUrl, RelayUsage, UncheckedUrl,
};
use nostr_types::{Event, Id, Profile, PublicKey, RelayUrl, RelayUsage};
use parking_lot::RwLock as PRwLock;
use regex::Regex;
use rhai::{Engine, AST};
@ -28,16 +28,6 @@ use tokio::sync::watch::Receiver as WatchReceiver;
use tokio::sync::watch::Sender as WatchSender;
use tokio::sync::{broadcast, mpsc, Mutex, Notify, RwLock};
/// The state that a Zap is in (it moves through 5 states before it is complete)
#[derive(Debug, Clone)]
pub enum ZapState {
None,
CheckingLnurl(Id, PublicKey, UncheckedUrl),
SeekingAmount(Id, PublicKey, PayRequestData, UncheckedUrl),
LoadingInvoice(Id, PublicKey),
ReadyToPay(Id, String), // String is the Zap Invoice as a string, to be shown as a QR code
}
/// Global data shared between threads. Access via the static ref `GLOBALS`.
pub struct Globals {
/// This is a broadcast channel. All Minions should listen on it.
@ -68,9 +58,15 @@ pub struct Globals {
/// All nostr people records currently loaded into memory, keyed by pubkey
pub people: People,
/// The relays currently connected to
/// The relays currently connected to. It tracks the jobs that relay is assigned.
/// As the minion completes jobs, code modifies this jobset, removing those completed
/// jobs.
pub connected_relays: DashMap<RelayUrl, Vec<RelayJob>>,
/// The relays not connected to, and which we will not connect to again until some
/// time passes, but which we still have jobs for
pub penalty_box_relays: DashMap<RelayUrl, Vec<RelayJob>>,
/// The relay picker, used to pick the next relay
pub relay_picker: RelayPicker<Hooks>,
@ -86,6 +82,9 @@ pub struct Globals {
/// Fetcher
pub fetcher: Fetcher,
/// Seeker
pub seeker: Seeker,
/// Failed Avatars
/// If in this map, the avatar failed to load or process and is unrecoverable
/// (but we will take them out and try again if new metadata flows in)
@ -197,11 +196,13 @@ lazy_static! {
tmp_overlord_receiver: Mutex::new(Some(tmp_overlord_receiver)),
people: People::new(),
connected_relays: DashMap::new(),
penalty_box_relays: DashMap::new(),
relay_picker: Default::default(),
identity: GossipIdentity::default(),
dismissed: RwLock::new(Vec::new()),
feed: Feed::new(),
fetcher: Fetcher::new(),
seeker: Seeker::new(),
failed_avatars: RwLock::new(HashSet::new()),
pixels_per_point_times_100: AtomicU32::new(139), // 100 dpi, 1/72th inch => 1.38888
status_queue: PRwLock::new(StatusQueue::new(

View File

@ -90,7 +90,7 @@ pub use fetcher::Fetcher;
mod filter;
mod globals;
pub use globals::{Globals, ZapState, GLOBALS};
pub use globals::{Globals, GLOBALS};
mod gossip_identity;
pub use gossip_identity::GossipIdentity;
@ -98,6 +98,9 @@ pub use gossip_identity::GossipIdentity;
mod media;
pub use media::Media;
mod misc;
pub use misc::ZapState;
/// Rendering various names of users
pub mod names;
@ -133,6 +136,9 @@ pub use relay::Relay;
mod relay_picker_hooks;
pub use relay_picker_hooks::Hooks;
mod seeker;
pub use seeker::Seeker;
mod status;
pub use status::StatusQueue;
@ -240,11 +246,15 @@ pub async fn run() {
// Start the fetcher
crate::fetcher::Fetcher::start();
// Start the seeker
crate::seeker::Seeker::start();
// Start periodic tasks in people manager (after signer)
crate::people::People::start();
// Start periodic tasks in pending
crate::pending::start();
// DISABLED until the UI is ready to implement.
// crate::pending::start();
// Start long-lived subscriptions
// (this also does a relay_picker init)

18
gossip-lib/src/misc.rs Normal file
View File

@ -0,0 +1,18 @@
use nostr_types::{Id, PayRequestData, PublicKey, UncheckedUrl};
/// The state that a Zap is in (it moves through 5 states before it is complete)
#[derive(Debug, Clone)]
pub enum ZapState {
None,
CheckingLnurl(Id, PublicKey, UncheckedUrl),
SeekingAmount(Id, PublicKey, PayRequestData, UncheckedUrl),
LoadingInvoice(Id, PublicKey),
ReadyToPay(Id, String), // String is the Zap Invoice as a string, to be shown as a QR code
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Freshness {
NeverSought,
Stale,
Fresh,
}

View File

@ -122,7 +122,7 @@ pub async fn get_and_follow_nip05(
update_relays(&nip05, nip05file, &pubkey).await?;
// Follow
GLOBALS.people.follow(&pubkey, true, list, public, true)?;
GLOBALS.people.follow(&pubkey, true, list, public)?;
tracing::info!("Followed {}", &nip05);

View File

@ -29,32 +29,6 @@ pub fn general_feed(
filters
}
pub fn relay_lists(authors: &[PublicKey]) -> Vec<Filter> {
// Try to find where people post.
// Subscribe to kind-10002 `RelayList`s to see where people post.
// Subscribe to ContactLists so we can look at the contents and
// divine relays people write to (if using a client that does that).
// BUT ONLY for people where this kind of data hasn't been received
// in the last 8 hours (so we don't do it every client restart).
let keys_needing_relay_lists: Vec<PublicKeyHex> = GLOBALS
.people
.get_subscribed_pubkeys_needing_relay_lists(authors)
.drain(..)
.map(|pk| pk.into())
.collect();
if !keys_needing_relay_lists.is_empty() {
vec![Filter {
authors: keys_needing_relay_lists,
kinds: vec![EventKind::RelayList, EventKind::ContactList],
// No since. These are replaceable events, we should only get 1 per person.
..Default::default()
}]
} else {
vec![]
}
}
pub fn augments(ids: &[IdHex]) -> Vec<Filter> {
let event_kinds = crate::feed::feed_augment_event_kinds();
@ -176,6 +150,8 @@ pub fn outbox(since: Unixtime) -> Vec<Filter> {
}
}
// This FORCES the fetch of relay lists without checking if we need them.
// See also relay_lists() which checks if they are needed first.
pub fn discover(pubkeys: &[PublicKey]) -> Vec<Filter> {
let pkp: Vec<PublicKeyHex> = pubkeys.iter().map(|pk| pk.into()).collect();
vec![Filter {

View File

@ -40,7 +40,7 @@ impl Minion {
}
}
if !it_matches {
tracing::info!(
tracing::debug!(
"{} sent event that does not match filters on subscription {}: {}",
self.url,
handle,

View File

@ -638,9 +638,7 @@ impl Minion {
}
};
let mut filters = filter_fns::general_feed(&self.general_feed_keys, since, None);
let filters2 = filter_fns::relay_lists(&self.general_feed_keys);
filters.extend(filters2);
let filters = filter_fns::general_feed(&self.general_feed_keys, since, None);
if filters.is_empty() {
self.unsubscribe("general_feed").await?;
@ -682,9 +680,7 @@ impl Minion {
None => self.compute_since(GLOBALS.storage.read_setting_feed_chunk()),
};
let mut filters = filter_fns::general_feed(&new_keys, since, None);
let filters2 = filter_fns::relay_lists(&new_keys);
filters.extend(filters2);
let filters = filter_fns::general_feed(&new_keys, since, None);
if !filters.is_empty() {
self.subscribe(filters, "temp_general_feed_update", job_id)

View File

@ -7,7 +7,8 @@ use crate::comms::{
use crate::dm_channel::DmChannel;
use crate::error::{Error, ErrorKind};
use crate::feed::FeedKind;
use crate::globals::{Globals, ZapState, GLOBALS};
use crate::globals::{Globals, GLOBALS};
use crate::misc::ZapState;
use crate::nip46::{Approval, ParsedCommand};
use crate::people::{Person, PersonList};
use crate::person_relay::PersonRelay;
@ -362,6 +363,14 @@ impl Overlord {
}
refmut.value_mut().push(job);
}
} else if GLOBALS.penalty_box_relays.contains_key(&url) {
// It is in the penalty box.
// To avoid a race condition with the task that removes it from the penalty
// box we have to use entry to make sure it was still there
GLOBALS
.penalty_box_relays
.entry(url)
.and_modify(|existing_jobs| Self::extend_jobs(existing_jobs, jobs));
} else {
// Start up the minion
let mut minion = Minion::new(url.clone()).await?;
@ -400,11 +409,15 @@ impl Overlord {
// Remove from our hashmap
self.minions_task_url.remove(&id);
// Set to not connected
let relayjobs = GLOBALS.connected_relays.remove(&url).map(|(_, v)| v);
// Set to not connected, and take any unfinished jobs
let mut relayjobs = match GLOBALS.connected_relays.remove(&url).map(|(_, v)| v) {
Some(jobs) => jobs,
None => vec![],
};
// Exclusion will be non-zero if there was a failure. It will be zero if we
// succeeded
let mut exclusion: u64;
let mut completed: bool = false;
match join_result {
Err(join_error) => {
@ -421,36 +434,37 @@ impl Overlord {
}
exclusion = match exitreason {
MinionExitReason::GotDisconnected => 120,
MinionExitReason::GotShutdownMessage => 0,
MinionExitReason::GotWSClose => 120,
MinionExitReason::LostOverlord => 0,
MinionExitReason::SubscriptionsHaveCompleted => {
relayjobs = vec![];
0
}
MinionExitReason::Unknown => 120,
MinionExitReason::SubscriptionsHaveCompleted => 5,
_ => 5,
};
// Remember if the relay says all the jobs have completed
if matches!(exitreason, MinionExitReason::SubscriptionsHaveCompleted) {
completed = true;
}
}
Err(e) => {
Self::bump_failure_count(&url);
tracing::error!("Minion {} completed with error: {}", &url, e);
exclusion = 120;
if let ErrorKind::RelayRejectedUs = e.kind {
exclusion = 60 * 60 * 24 * 365; // don't connect again, practically
exclusion = u64::MAX;
} else if let ErrorKind::ReqwestHttpError(_) = e.kind {
exclusion = u64::MAX;
} else if let ErrorKind::Websocket(wserror) = e.kind {
if let tungstenite::error::Error::Http(response) = wserror {
exclusion = match response.status() {
StatusCode::MOVED_PERMANENTLY => 60 * 60 * 24,
StatusCode::PERMANENT_REDIRECT => 60 * 60 * 24,
StatusCode::UNAUTHORIZED => 60 * 60 * 24,
StatusCode::PAYMENT_REQUIRED => 60 * 60 * 24,
StatusCode::FORBIDDEN => 60 * 60 * 24,
StatusCode::NOT_FOUND => 60 * 60 * 24,
StatusCode::PROXY_AUTHENTICATION_REQUIRED => 60 * 60 * 24,
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => 60 * 60 * 24,
StatusCode::NOT_IMPLEMENTED => 60 * 60 * 24,
StatusCode::BAD_GATEWAY => 60 * 60 * 24,
StatusCode::MOVED_PERMANENTLY => u64::MAX,
StatusCode::PERMANENT_REDIRECT => u64::MAX,
StatusCode::UNAUTHORIZED => u64::MAX,
StatusCode::PAYMENT_REQUIRED => u64::MAX,
StatusCode::FORBIDDEN => u64::MAX,
StatusCode::NOT_FOUND => u64::MAX,
StatusCode::PROXY_AUTHENTICATION_REQUIRED => u64::MAX,
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => u64::MAX,
StatusCode::NOT_IMPLEMENTED => u64::MAX,
StatusCode::BAD_GATEWAY => u64::MAX,
s if s.as_u16() >= 400 => 120,
_ => 120,
};
@ -471,9 +485,9 @@ impl Overlord {
},
};
// We might need to act upon this minion exiting
// Act upon this minion exiting, unless we are quitting
if self.read_runstate.borrow().going_online() {
self.recover_from_minion_exit(url, relayjobs, exclusion, completed)
self.recover_from_minion_exit(url, relayjobs, exclusion)
.await;
}
}
@ -481,9 +495,8 @@ impl Overlord {
async fn recover_from_minion_exit(
&mut self,
url: RelayUrl,
jobs: Option<Vec<RelayJob>>,
jobs: Vec<RelayJob>,
exclusion: u64,
completed: bool,
) {
// Let the relay picker know it disconnected
GLOBALS
@ -496,38 +509,52 @@ impl Overlord {
}
self.pick_relays().await;
if let Some(mut jobs) = jobs {
// Remove any advertise jobs from the active set
for job in &jobs {
GLOBALS.active_advertise_jobs.remove(&job.payload.job_id);
}
if completed {
return;
}
// If we have any persistent jobs, restart after a delay
let persistent_jobs: Vec<RelayJob> = jobs
.drain(..)
.filter(|job| job.reason.persistent())
.collect();
if !persistent_jobs.is_empty() {
// Do it after a delay
std::mem::drop(tokio::spawn(async move {
// Delay for exclusion first
tracing::info!(
"Minion {} will restart in {} seconds to continue persistent jobs",
&url,
exclusion
);
tokio::time::sleep(Duration::new(exclusion, 0)).await;
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::ReengageMinion(url, persistent_jobs));
}));
}
if exclusion == 0 {
return;
}
// Remove any advertise jobs from the active set
for job in &jobs {
GLOBALS.active_advertise_jobs.remove(&job.payload.job_id);
}
if jobs.is_empty() {
return;
}
// OK we have an exclusion and unfinished jobs.
//
// Add this relay to the penalty box, and setup a task to reengage
// it after the exclusion completes
let exclusion = exclusion.max(10); // safety catch, minimum exclusion is 10s
GLOBALS.penalty_box_relays.insert(url.clone(), jobs);
tracing::info!(
"Minion {} will restart in {} seconds to continue persistent jobs",
&url,
exclusion
);
if exclusion != u64::MAX {
// Re-engage after the delay
std::mem::drop(tokio::spawn(async move {
tokio::time::sleep(Duration::new(exclusion, 0)).await;
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::ReengageMinion(url));
}));
}
// otherwise leave it in the penalty box forever
}
async fn reengage_minion(&mut self, url: RelayUrl) -> Result<(), Error> {
// Take from penalty box
if let Some(pair) = GLOBALS.penalty_box_relays.remove(&url) {
self.engage_minion(url, pair.1).await?;
}
Ok(())
}
fn bump_failure_count(url: &RelayUrl) {
@ -537,6 +564,14 @@ impl Overlord {
}
}
fn extend_jobs(jobs: &mut Vec<RelayJob>, mut more: Vec<RelayJob>) {
for newjob in more.drain(..) {
if !jobs.iter().any(|job| job.matches(&newjob)) {
jobs.push(newjob)
}
}
}
async fn handle_message(&mut self, message: ToOverlordMessage) -> Result<(), Error> {
match message {
ToOverlordMessage::AddPubkeyRelay(pubkey, relayurl) => {
@ -683,8 +718,8 @@ impl Overlord {
ToOverlordMessage::RankRelay(relay_url, rank) => {
Self::rank_relay(relay_url, rank)?;
}
ToOverlordMessage::ReengageMinion(url, persistent_jobs) => {
self.engage_minion(url, persistent_jobs).await?;
ToOverlordMessage::ReengageMinion(url) => {
self.reengage_minion(url).await?;
}
ToOverlordMessage::RefreshSubscribedMetadata => {
self.refresh_subscribed_metadata().await?;
@ -1319,7 +1354,21 @@ impl Overlord {
}
/// Fetch an event from a specific relay by event `Id`
pub async fn fetch_event(&mut self, id: Id, relay_urls: Vec<RelayUrl>) -> Result<(), Error> {
pub async fn fetch_event(
&mut self,
id: Id,
mut relay_urls: Vec<RelayUrl>,
) -> Result<(), Error> {
// Use READ relays if relays are unknown
if relay_urls.is_empty() {
relay_urls = GLOBALS
.storage
.filter_relays(|r| r.has_usage_bits(Relay::READ) && r.rank != 0)?
.iter()
.map(|relay| relay.url.clone())
.collect();
}
// Don't do this if we already have the event
if !GLOBALS.storage.has_event(id)? {
// Note: minions will remember if they get the same id multiple times
@ -1371,7 +1420,7 @@ impl Overlord {
list: PersonList,
public: bool,
) -> Result<(), Error> {
GLOBALS.people.follow(&pubkey, true, list, public, true)?;
GLOBALS.people.follow(&pubkey, true, list, public)?;
tracing::debug!("Followed {}", &pubkey.as_hex_string());
Ok(())
}
@ -1415,7 +1464,7 @@ impl Overlord {
// Follow
GLOBALS
.people
.follow(&nprofile.pubkey, true, list, public, true)?;
.follow(&nprofile.pubkey, true, list, public)?;
GLOBALS
.status_queue
@ -2766,8 +2815,8 @@ impl Overlord {
}
// Separately subscribe to RelayList discovery for everyone we follow
// We just do this once at startup. Relay lists don't change that frequently.
let followed = GLOBALS.people.get_subscribed_pubkeys();
// who needs to seek a relay list again.
let followed = GLOBALS.people.get_subscribed_pubkeys_needing_relay_lists();
self.subscribe_discover(followed, None).await?;
// Separately subscribe to our outbox events on our write relays
@ -2825,11 +2874,27 @@ impl Overlord {
/// Subscribe to the multiple user's relay lists (optionally on the given relays, otherwise using
/// theconfigured discover relays)
///
/// Caller should probably check Person.relay_list_last_sought first to make sure we don't
/// already have an in-flight request doing this. This can be done with:
/// GLOBALS.people.person_needs_relay_list()
/// GLOBALS.people.get_subscribed_pubkeys_needing_relay_lists()
pub async fn subscribe_discover(
&mut self,
pubkeys: Vec<PublicKey>,
relays: Option<Vec<RelayUrl>>,
) -> Result<(), Error> {
// Mark for each person that we are seeking their relay list
// so that we don't repeat this for a while
let now = Unixtime::now().unwrap();
let mut txn = GLOBALS.storage.get_write_txn()?;
for pk in pubkeys.iter() {
let mut person = GLOBALS.storage.read_or_create_person(pk, Some(&mut txn))?;
person.relay_list_last_sought = now.0;
GLOBALS.storage.write_person(&person, Some(&mut txn))?;
}
txn.commit()?;
// Discover their relays
let discover_relay_urls: Vec<RelayUrl> = match relays {
Some(r) => r,

View File

@ -78,6 +78,7 @@ impl Pending {
}
}
#[allow(dead_code)]
pub fn start() {
task::spawn(async {
let mut read_runstate = GLOBALS.read_runstate.clone();

View File

@ -1,6 +1,7 @@
use crate::comms::ToOverlordMessage;
use crate::error::{Error, ErrorKind};
use crate::globals::GLOBALS;
use crate::misc::Freshness;
use dashmap::{DashMap, DashSet};
use image::RgbaImage;
use nostr_types::{
@ -106,11 +107,19 @@ impl People {
}
/// Get all the pubkeys that the user subscribes to in any list
/// (We also force the current user into this list)
pub fn get_subscribed_pubkeys(&self) -> Vec<PublicKey> {
// We subscribe to all people in all lists.
// This is no longer synonomous with the ContactList list
match GLOBALS.storage.get_people_in_all_followed_lists() {
Ok(people) => people,
Ok(mut people) => {
if let Some(pk) = GLOBALS.identity.public_key() {
if !people.contains(&pk) {
people.push(pk);
}
}
people
}
Err(e) => {
tracing::error!("{}", e);
vec![]
@ -128,27 +137,46 @@ impl People {
}
/// Get all the pubkeys that need relay lists (from the given set)
pub fn get_subscribed_pubkeys_needing_relay_lists(
&self,
among_these: &[PublicKey],
) -> Vec<PublicKey> {
pub fn get_subscribed_pubkeys_needing_relay_lists(&self) -> Vec<PublicKey> {
let stale = Unixtime::now().unwrap().0
- 60 * 60
* GLOBALS
.storage
.read_setting_relay_list_becomes_stale_hours() as i64;
if let Ok(vec) = GLOBALS.storage.filter_people(|p| {
p.is_subscribed_to()
&& p.relay_list_last_received < stale
&& among_these.contains(&p.pubkey)
}) {
if let Ok(vec) = GLOBALS
.storage
.filter_people(|p| p.is_subscribed_to() && p.relay_list_last_sought < stale)
{
vec.iter().map(|p| p.pubkey).collect()
} else {
vec![]
}
}
/// Get if a person needs a relay list
pub fn person_needs_relay_list(pubkey: PublicKey) -> Freshness {
let staletime = Unixtime::now().unwrap().0
- 60 * 60
* GLOBALS
.storage
.read_setting_relay_list_becomes_stale_hours() as i64;
match GLOBALS.storage.read_person(&pubkey) {
Err(_) => Freshness::NeverSought,
Ok(None) => Freshness::NeverSought,
Ok(Some(p)) => {
if p.relay_list_last_sought == 0 {
Freshness::NeverSought
} else if p.relay_list_last_sought < staletime {
Freshness::Stale
} else {
Freshness::Fresh
}
}
}
}
/// Create person record for this pubkey, if missing
pub fn create_if_missing(&self, pubkey: PublicKey) {
if let Err(e) = self.create_all_if_missing(&[pubkey]) {
@ -729,7 +757,6 @@ impl People {
follow: bool,
list: PersonList,
public: bool,
discover: bool, // if you also want to subscribe to their relay list
) -> Result<(), Error> {
if follow {
GLOBALS
@ -738,6 +765,18 @@ impl People {
// Add to the relay picker. If they are already there, it will be ok.
GLOBALS.relay_picker.add_someone(*pubkey)?;
// Maybe seek relay list (if needed)
let seek_relay_list = match Self::person_needs_relay_list(*pubkey) {
Freshness::NeverSought => true,
Freshness::Stale => true,
Freshness::Fresh => false,
};
if seek_relay_list {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::SubscribeDiscover(vec![*pubkey], None));
};
} else {
GLOBALS
.storage
@ -753,12 +792,6 @@ impl People {
.to_overlord
.send(ToOverlordMessage::RefreshScoresAndPickRelays);
if follow && discover {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::SubscribeDiscover(vec![*pubkey], None));
}
Ok(())
}
@ -817,8 +850,6 @@ impl People {
pubkey: PublicKey,
created_at: i64,
) -> Result<bool, Error> {
let now = Unixtime::now().unwrap().0;
let mut retval = false;
let mut person = match GLOBALS.storage.read_person(&pubkey)? {
@ -826,7 +857,6 @@ impl People {
None => Person::new(pubkey),
};
person.relay_list_last_received = now;
if let Some(old_at) = person.relay_list_created_at {
if created_at < old_at {
retval = false;

View File

@ -239,6 +239,9 @@ pub async fn process_new_event(
// Invalidate UI events indicated by those relationships
GLOBALS.ui_notes_to_invalidate.write().extend(&invalid_ids);
// Let seeker know about this event id (in case it was sought)
GLOBALS.seeker.found_or_cancel(event.id);
// If metadata, update person
if event.kind == EventKind::Metadata {
let metadata: Metadata = serde_json::from_str(&event.content)?;
@ -273,26 +276,43 @@ pub async fn process_new_event(
} else if event.kind == EventKind::RelayList {
GLOBALS.storage.process_relay_list(event)?;
// Let the seeker know we now have relays for this author, in case the seeker
// wants to update it's state
// (we might not, but by this point we have tried)
GLOBALS.seeker.found_author_relays(event.pubkey);
// the following also refreshes scores before it picks relays
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::RefreshScoresAndPickRelays);
} else if event.kind == EventKind::Repost {
// If the content is a repost, seek the event it reposts
for eref in event.mentions().iter() {
match eref {
EventReference::Id(id, optrelay, _marker) => {
if let Some(rurl) = optrelay {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::FetchEvent(*id, vec![rurl.to_owned()]));
// If it has a json encoded inner event
if let Ok(inner_event) = serde_json::from_str::<Event>(&event.content) {
// Seek that event by id and author
GLOBALS
.seeker
.seek_id_and_author(inner_event.id, inner_event.pubkey)?;
} else {
// If the content is a repost, seek the event it reposts
for eref in event.mentions().iter() {
match eref {
EventReference::Id(id, optrelay, _marker) => {
if let Some(rurl) = optrelay {
GLOBALS
.seeker
.seek_id_and_relays(*id, vec![rurl.to_owned()]);
} else {
// Even if the event tags the author, we have no way to coorelate
// the nevent with that tag.
GLOBALS.seeker.seek_id(*id);
}
}
}
EventReference::Addr(ea) => {
if !ea.relays.is_empty() {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::FetchEventAddr(ea.clone()));
EventReference::Addr(ea) => {
if !ea.relays.is_empty() {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::FetchEventAddr(ea.clone()));
}
}
}
}

View File

@ -30,6 +30,13 @@ pub struct Profile {
impl Profile {
fn new() -> Result<Profile, Error> {
if cfg!(feature = "appimage") {
// Because AppImage only changes $HOME (and not $XDG_DATA_HOME), we unset
// $XDG_DATA_HOME and let it use the changed $HOME on linux to find the
// data directory
std::env::remove_var("XDG_DATA_HOME");
}
// Get system standard directory for user data
let data_dir = dirs::data_dir()
.ok_or::<Error>("Cannot find a directory to store application data.".into())?;

234
gossip-lib/src/seeker.rs Normal file
View File

@ -0,0 +1,234 @@
use crate::comms::ToOverlordMessage;
use crate::error::Error;
use crate::globals::GLOBALS;
use crate::misc::Freshness;
use crate::people::People;
use dashmap::DashMap;
use nostr_types::{Id, PublicKey, RelayUrl, RelayUsage, Unixtime};
use std::time::Duration;
use tokio::time::Instant;
#[derive(Debug, Clone)]
pub enum SeekState {
WaitingRelayList(Unixtime, PublicKey),
WaitingEvent(Unixtime),
}
#[derive(Debug, Default)]
pub struct Seeker {
events: DashMap<Id, SeekState>,
}
impl Seeker {
pub(crate) fn new() -> Seeker {
Seeker {
..Default::default()
}
}
fn get_relays(author: PublicKey) -> Result<Vec<RelayUrl>, Error> {
Ok(GLOBALS
.storage
.get_best_relays(author, RelayUsage::Outbox)?
.iter()
.map(|(r, _)| r.to_owned())
.collect())
}
fn seek_relay_list(author: PublicKey) {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::SubscribeDiscover(vec![author], None));
}
fn seek_event_at_our_read_relays(id: Id) {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::FetchEvent(id, vec![]));
}
fn seek_event_at_relays(id: Id, relays: Vec<RelayUrl>) {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::FetchEvent(id, relays));
}
/// Seek an event when you only have the `Id`
pub(crate) fn seek_id(&self, id: Id) {
if self.events.get(&id).is_some() {
return; // we are already seeking this event
}
tracing::debug!("Seeking id={}", id.as_hex_string());
Self::seek_event_at_our_read_relays(id);
// Remember when we asked
let now = Unixtime::now().unwrap();
self.events.insert(id, SeekState::WaitingEvent(now));
}
/// Seek an event when you have the `Id` and the author `PublicKey`
pub(crate) fn seek_id_and_author(&self, id: Id, author: PublicKey) -> Result<(), Error> {
if self.events.get(&id).is_some() {
return Ok(()); // we are already seeking this event
}
tracing::debug!(
"Seeking id={} with author={}",
id.as_hex_string(),
author.as_hex_string()
);
let when = Unixtime::now().unwrap();
// Check if we have the author's relay list
match People::person_needs_relay_list(author) {
Freshness::NeverSought => {
Self::seek_relay_list(author);
self.events
.insert(id, SeekState::WaitingRelayList(when, author));
}
Freshness::Stale => {
// Seek the relay list because it is stale, but don't let that hold us up
// using the stale data
Self::seek_relay_list(author);
let relays = Self::get_relays(author)?;
Self::seek_event_at_relays(id, relays);
self.events.insert(id, SeekState::WaitingEvent(when));
}
Freshness::Fresh => {
let relays = Self::get_relays(author)?;
Self::seek_event_at_relays(id, relays);
self.events.insert(id, SeekState::WaitingEvent(when));
}
}
Ok(())
}
/// Seek an event when you have the `Id` and the relays to seek from
pub(crate) fn seek_id_and_relays(&self, id: Id, relays: Vec<RelayUrl>) {
if let Some(existing) = self.events.get(&id) {
if matches!(existing.value(), SeekState::WaitingEvent(_)) {
return; // Already seeking it
}
}
let when = Unixtime::now().unwrap();
Self::seek_event_at_relays(id, relays);
self.events.insert(id, SeekState::WaitingEvent(when));
}
/// Inform the seeker that an author's relay list has just arrived
pub(crate) fn found_author_relays(&self, pubkey: PublicKey) {
// Instead of updating the map while we iterate (which could deadlock)
// we save updates here and apply when the iterator is finished.
let mut updates: Vec<(Id, SeekState)> = Vec::new();
for refmutmulti in self.events.iter_mut() {
if let SeekState::WaitingRelayList(_when, author) = refmutmulti.value() {
if *author == pubkey {
let now = Unixtime::now().unwrap();
let id = *refmutmulti.key();
if let Ok(relays) = Self::get_relays(*author) {
Self::seek_event_at_relays(id, relays);
updates.push((id, SeekState::WaitingEvent(now)));
}
}
}
}
for (id, state) in updates.drain(..) {
let _ = self.events.insert(id, state);
}
}
pub(crate) fn found_or_cancel(&self, id: Id) {
self.events.remove(&id);
}
pub(crate) fn start() {
// Setup periodic queue management
tokio::task::spawn(async move {
let mut read_runstate = GLOBALS.read_runstate.clone();
read_runstate.mark_unchanged();
if read_runstate.borrow().going_offline() {
return;
}
let sleep = tokio::time::sleep(Duration::from_millis(1000));
tokio::pin!(sleep);
loop {
tokio::select! {
_ = &mut sleep => {
sleep.as_mut().reset(Instant::now() + Duration::from_millis(1000));
},
_ = read_runstate.wait_for(|runstate| runstate.going_offline()) => break,
}
GLOBALS.seeker.run_once().await;
}
tracing::info!("Seeker shutdown");
});
}
pub(crate) async fn run_once(&self) {
if self.events.is_empty() {
return;
}
// Instead of updating the map while we iterate (which could deadlock)
// we save updates here and apply when the iterator is finished.
let mut updates: Vec<(Id, Option<SeekState>)> = Vec::new();
let now = Unixtime::now().unwrap();
for refmulti in self.events.iter() {
let id = *refmulti.key();
match refmulti.value() {
SeekState::WaitingRelayList(when, author) => {
// Check if we have their relays yet
match People::person_needs_relay_list(*author) {
Freshness::Fresh | Freshness::Stale => {
if let Ok(relays) = Self::get_relays(*author) {
Self::seek_event_at_relays(id, relays);
updates.push((id, Some(SeekState::WaitingEvent(now))));
continue;
}
}
_ => {}
}
// If it has been 15 seconds, give up the wait and seek from our READ relays
if now - *when > Duration::from_secs(15) {
Self::seek_event_at_our_read_relays(id);
updates.push((id, Some(SeekState::WaitingEvent(now))));
}
// Otherwise keep waiting
}
SeekState::WaitingEvent(when) => {
if now - *when > Duration::from_secs(15) {
tracing::debug!("Failed to find id={}", id.as_hex_string());
updates.push((id, None));
}
}
}
}
// Apply updates
for (id, replacement) in updates.drain(..) {
match replacement {
Some(state) => {
let _ = self.events.insert(id, state);
}
None => {
let _ = self.events.remove(&id);
}
}
}
}
}

View File

@ -32,7 +32,7 @@ impl Storage {
nip05_valid: person1.nip05_valid,
nip05_last_checked: person1.nip05_last_checked,
relay_list_created_at: person1.relay_list_created_at,
relay_list_last_received: person1.relay_list_last_received,
relay_list_last_sought: person1.relay_list_last_received,
};
self.write_person2(&person2, Some(txn))?;
count += 1;

View File

@ -791,6 +791,7 @@ impl Storage {
bool,
true
);
def_setting!(feed_newest_at_bottom, b"feed_newest_at_bottom", bool, false);
def_setting!(posting_area_at_top, b"posting_area_at_top", bool, true);
def_setting!(status_bar, b"status_bar", bool, false);
def_setting!(
@ -1100,7 +1101,7 @@ impl Storage {
//// Modify all relay records
#[inline]
pub(crate) fn modify_all_relays<'a, M>(
pub fn modify_all_relays<'a, M>(
&'a self,
modify: M,
rw_txn: Option<&mut RwTxn<'a>>,
@ -1163,16 +1164,16 @@ impl Storage {
pub fn process_relay_list(&self, event: &Event) -> Result<(), Error> {
let mut txn = self.env.write_txn()?;
// Check if this relay list is newer than the stamp we have for its author
if let Some(mut person) = self.read_person(&event.pubkey)? {
// Mark that we received it (changes fetch duration for next time)
person.relay_list_last_received = Unixtime::now().unwrap().0;
// Check if this relay list is newer than the stamp we have for its author
if let Some(previous_at) = person.relay_list_created_at {
if event.created_at.0 <= previous_at {
// This list is old. But let's save the last_received setting:
self.write_person(&person, Some(&mut txn))?;
return Ok(());
}
}
// If we got here, the list is new.
// Mark when it was created
person.relay_list_created_at = Some(event.created_at.0);
@ -2017,6 +2018,23 @@ impl Storage {
self.read_person2(pubkey)
}
/// Read a person record, create if missing
#[inline]
pub fn read_or_create_person<'a>(
&'a self,
pubkey: &PublicKey,
rw_txn: Option<&mut RwTxn<'a>>,
) -> Result<Person, Error> {
match self.read_person(pubkey)? {
Some(p) => Ok(p),
None => {
let person = Person::new(pubkey.to_owned());
self.write_person(&person, rw_txn)?;
Ok(person)
}
}
}
/// Write a new person record only if missing
pub fn write_person_if_missing<'a>(
&'a self,

View File

@ -34,9 +34,10 @@ pub struct Person2 {
/// for an update)
pub relay_list_created_at: Option<i64>,
/// When their relay list was last received (to determine if we need to
/// When their relay list was last sought (to determine if we need to
/// check for an update)
pub relay_list_last_received: i64,
#[serde(rename = "relay_list_last_received")]
pub relay_list_last_sought: i64,
}
impl Person2 {
@ -50,7 +51,7 @@ impl Person2 {
nip05_valid: false,
nip05_last_checked: None,
relay_list_created_at: None,
relay_list_last_received: 0,
relay_list_last_sought: 0,
}
}

View File

@ -18,18 +18,44 @@ trap cleanup EXIT
cd ../..
RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable"
cargo build --features=lang-cjk --release
cargo build --features=lang-cjk,appimage --release
cd target/
rm -rf ./appimage
mkdir -p appimage
cd appimage/
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
# Build AppDir
mkdir -p AppDir/usr/bin/
mkdir -p AppDir/usr/lib/
mkdir -p AppDir/usr/share/applications/
mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps/
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps/
mkdir -p AppDir/usr/share/icons/hicolor/128x128/apps/
mkdir -p AppDir/usr/share/icons/hicolor/64x64/apps/
mkdir -p AppDir/usr/share/icons/hicolor/32x32/apps/
mkdir -p AppDir/usr/share/icons/hicolor/16x16/apps/
cp ../release/gossip AppDir/usr/bin/gossip
strip AppDir/usr/bin/gossip
cp ../../logo/gossip.png AppDir/usr/share/icons/hicolor/128x128/apps/gossip.png
cp ../../logo/gossip.svg AppDir/usr/share/icons/hicolor/scalable/apps/gossip.svg
cp ../../packaging/debian/gossip.desktop AppDir/usr/share/applications/gossip.desktop
ln -s usr/bin/gossip AppDir/AppRun
ln -s gossip.png AppDir/.DirIcon
ln -s usr/share/applications/gossip.desktop AppDir/gossip.desktop
ln -s usr/share/icons/hicolor/128x128/apps/gossip.png AppDir/gossip.png
./linuxdeploy-x86_64.AppImage --appdir AppDir -e ../release/gossip -i ../../logo/gossip.png -d ../../packaging/debian/gossip.desktop --output=appimage
# Get appimagetool
wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod a+x appimagetool-x86_64.AppImage
echo "AppImage is at ../../target/appimage/gossip-x86_64.AppImage"
# Use appimagetool to build the AppImage
./appimagetool-x86_64.AppImage AppDir
# Bundle for portable mode
mkdir -p gossip-x86_64.AppImage.home/.local/share
tar cvf - gossip-x86_64.AppImage gossip-x86_64.AppImage.home | gzip -c > gossip-x86_64.AppImage.tar.gz
echo "Portable AppImage is at ../../target/appimage/gossip-x86_64.AppImage.tar.gz"
exit 0