add groups registry
This commit is contained in:
@@ -1,3 +1,380 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
|
||||
mod group;
|
||||
mod protocol;
|
||||
|
||||
pub use group::*;
|
||||
pub use protocol::*;
|
||||
|
||||
pub(crate) static LOCAL_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
|
||||
|
||||
const LIST_SUBSCRIPTION: &str = "nip29-list";
|
||||
const GROUP_PREFIX: &str = "grp-";
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
GroupsRegistry::set_global(cx.new(GroupsRegistry::new), cx);
|
||||
}
|
||||
|
||||
struct GlobalGroupsRegistry(Entity<GroupsRegistry>);
|
||||
|
||||
impl Global for GlobalGroupsRegistry {}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GroupsEvent {
|
||||
Updated,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Signal {
|
||||
Event(SubscriptionId, Event),
|
||||
Eose(SubscriptionId),
|
||||
Closed(SubscriptionId, String),
|
||||
}
|
||||
|
||||
fn list_subscription_id() -> SubscriptionId {
|
||||
SubscriptionId::new(LIST_SUBSCRIPTION)
|
||||
}
|
||||
|
||||
pub(crate) fn subscription_id(key: &GroupKey) -> SubscriptionId {
|
||||
SubscriptionId::new(format!("{GROUP_PREFIX}{:08x}", key.uniq_id() as u32))
|
||||
}
|
||||
|
||||
fn is_group_subscription(id: &SubscriptionId) -> bool {
|
||||
let id = id.as_str();
|
||||
id == LIST_SUBSCRIPTION || id.starts_with(GROUP_PREFIX)
|
||||
}
|
||||
|
||||
impl EventEmitter<GroupsEvent> for GroupsRegistry {}
|
||||
|
||||
pub struct GroupsRegistry {
|
||||
groups: Vec<Entity<Group>>,
|
||||
list: GroupList,
|
||||
routes: HashMap<SubscriptionId, GroupKey>,
|
||||
signal_tx: flume::Sender<Signal>,
|
||||
signal_rx: flume::Receiver<Signal>,
|
||||
notification_listener: Option<Task<()>>,
|
||||
signal_consumer: Option<Task<()>>,
|
||||
tasks: SmallVec<[Task<()>; 2]>,
|
||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl GroupsRegistry {
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
cx.global::<GlobalGroupsRegistry>().0.clone()
|
||||
}
|
||||
|
||||
fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||
cx.set_global(GlobalGroupsRegistry(state));
|
||||
}
|
||||
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let this = cx.entity().downgrade();
|
||||
|
||||
let (tx, rx) = flume::unbounded::<Signal>();
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.subscribe(&nostr, |this, _nostr, event, cx| {
|
||||
if event.signer_changed() {
|
||||
this.reset(cx);
|
||||
this.handle_notifications(cx);
|
||||
this.load(cx);
|
||||
}
|
||||
}));
|
||||
|
||||
cx.defer(move |cx| {
|
||||
if let Err(error) = this.update(cx, |this, cx| {
|
||||
this.handle_notifications(cx);
|
||||
|
||||
if nostr.read(cx).current_user().is_some() {
|
||||
this.load(cx);
|
||||
}
|
||||
}) {
|
||||
log::warn!("nip29: initializing the registry failed: {error}");
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
groups: Vec::new(),
|
||||
list: GroupList::default(),
|
||||
routes: HashMap::new(),
|
||||
signal_tx: tx,
|
||||
signal_rx: rx,
|
||||
notification_listener: None,
|
||||
signal_consumer: None,
|
||||
tasks: smallvec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn groups(&self) -> &[Entity<Group>] {
|
||||
&self.groups
|
||||
}
|
||||
|
||||
pub fn group(&self, key: &GroupKey, cx: &App) -> Option<Entity<Group>> {
|
||||
self.groups
|
||||
.iter()
|
||||
.find(|group| group.read(cx).key() == key)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, cx: &mut Context<Self>) {
|
||||
self.notification_listener = None;
|
||||
self.signal_consumer = None;
|
||||
self.tasks.clear();
|
||||
|
||||
let routes = std::mem::take(&mut self.routes);
|
||||
|
||||
self.groups.clear();
|
||||
self.list = GroupList::default();
|
||||
|
||||
let client = NostrRegistry::global(cx).read(cx).client();
|
||||
|
||||
let mut ids: Vec<SubscriptionId> = routes.into_keys().collect();
|
||||
ids.push(list_subscription_id());
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
for id in ids {
|
||||
if let Err(error) = client.unsubscribe(&id).await {
|
||||
log::warn!("nip29: unsubscribing {id} failed: {error}");
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn load(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let task: Task<Result<GroupList, Error>> = cx.background_spawn(async move {
|
||||
let me = signer.get_public_key_async().await?;
|
||||
let filter = Filter::new().author(me).kind(Kind::SimpleGroups).limit(1);
|
||||
|
||||
client
|
||||
.subscribe(ReqTarget::auto([filter.clone()]))
|
||||
.with_id(list_subscription_id())
|
||||
.await?;
|
||||
|
||||
match client.database().query(filter).await?.into_iter().next() {
|
||||
Some(event) => GroupList::parse(&event),
|
||||
None => Ok(GroupList::default()),
|
||||
}
|
||||
});
|
||||
|
||||
self.tasks
|
||||
.push(cx.spawn(async move |this, cx| match task.await {
|
||||
Ok(list) => {
|
||||
if let Err(error) = this.update(cx, |this, cx| this.merge(list, cx)) {
|
||||
log::warn!("nip29: applying the group list failed: {error}");
|
||||
}
|
||||
}
|
||||
Err(error) => log::warn!("nip29: loading the group list failed: {error}"),
|
||||
}));
|
||||
}
|
||||
|
||||
fn merge(&mut self, list: GroupList, cx: &mut Context<Self>) {
|
||||
let entries = list.entries().to_vec();
|
||||
|
||||
for entry in entries {
|
||||
self.list.upsert(entry.clone());
|
||||
self.ensure(GroupKey::new(entry.relay.clone(), entry.id.clone()), cx);
|
||||
}
|
||||
|
||||
cx.emit(GroupsEvent::Updated);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn ensure(&mut self, key: GroupKey, cx: &mut Context<Self>) {
|
||||
if self.group(&key, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let group = cx.new(|_| Group::new(key.clone()));
|
||||
self.groups.push(group);
|
||||
|
||||
self.subscribe(key.clone(), cx);
|
||||
self.reload(key, None, cx);
|
||||
}
|
||||
|
||||
fn subscribe(&mut self, key: GroupKey, cx: &mut Context<Self>) {
|
||||
let Some(group) = self.group(&key, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.routes.insert(subscription_id(&key), key);
|
||||
|
||||
group.update(cx, |group, cx| {
|
||||
group.connect(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn reload(&mut self, key: GroupKey, event: Option<Event>, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
|
||||
let Some(group) = self.group(&key, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(me) = nostr.read(cx).current_user() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let logged = key.clone();
|
||||
let load = group.read(cx).load_envelopes(cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||
if let Some(event) = event
|
||||
&& let Err(error) = group
|
||||
.read_with(cx, |group, cx| group.save_envelope(event, cx))
|
||||
.await
|
||||
{
|
||||
log::warn!("nip29: caching failed: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
match load.await {
|
||||
Ok(events) => {
|
||||
group.update(cx, |group, cx| {
|
||||
group.hydrate(events, me, cx);
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!("nip29: reloading {} failed: {error}", logged.id());
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, id: SubscriptionId, event: Event, cx: &mut Context<Self>) {
|
||||
if id == list_subscription_id() {
|
||||
match GroupList::parse(&event) {
|
||||
Ok(list) => self.merge(list, cx),
|
||||
Err(error) => log::warn!("nip29: ignoring a malformed group list: {error}"),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(key) = self.routes.get(&id).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.reload(key, Some(event), cx);
|
||||
}
|
||||
|
||||
fn handle_eose(&mut self, id: SubscriptionId, cx: &mut Context<Self>) {
|
||||
let Some(key) = self.routes.get(&id).cloned() else {
|
||||
return;
|
||||
};
|
||||
let Some(group) = self.group(&key, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
group.update(cx, |group, cx| {
|
||||
group.eose(cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_closed(&mut self, id: SubscriptionId, reason: String, cx: &mut Context<Self>) {
|
||||
let Some(key) = self.routes.get(&id).cloned() else {
|
||||
return;
|
||||
};
|
||||
let Some(group) = self.group(&key, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
group.update(cx, |group, cx| {
|
||||
group.refuse(reason, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
||||
self.notification_listener = None;
|
||||
self.signal_consumer = None;
|
||||
|
||||
let nostr = NostrRegistry::global(cx).read(cx);
|
||||
let client = nostr.client();
|
||||
|
||||
let tx = self.signal_tx.clone();
|
||||
let rx = self.signal_rx.clone();
|
||||
|
||||
self.notification_listener = Some(cx.background_spawn(async move {
|
||||
let mut notifications = client.notifications();
|
||||
let mut processed = HashSet::new();
|
||||
const MAX_PROCESSED: usize = 10_000;
|
||||
|
||||
while let Some(notification) = notifications.next().await {
|
||||
let ClientNotification::Message { message, .. } = notification else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let signal = match *message {
|
||||
RelayMessage::Event {
|
||||
subscription_id,
|
||||
event,
|
||||
} => {
|
||||
let id = subscription_id.into_owned();
|
||||
|
||||
if !is_group_subscription(&id) {
|
||||
None
|
||||
} else {
|
||||
if processed.len() >= MAX_PROCESSED {
|
||||
processed.clear();
|
||||
}
|
||||
|
||||
processed
|
||||
.insert(event.id)
|
||||
.then(|| Signal::Event(id, event.into_owned()))
|
||||
}
|
||||
}
|
||||
RelayMessage::EndOfStoredEvents(id) => {
|
||||
let id = id.into_owned();
|
||||
is_group_subscription(&id).then_some(Signal::Eose(id))
|
||||
}
|
||||
RelayMessage::Closed {
|
||||
subscription_id,
|
||||
message,
|
||||
} => {
|
||||
let id = subscription_id.into_owned();
|
||||
is_group_subscription(&id)
|
||||
.then_some(Signal::Closed(id, message.into_owned()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(signal) = signal {
|
||||
tx.send_async(signal).await.ok();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
|
||||
while let Ok(signal) = rx.recv_async().await {
|
||||
this.update(cx, |this, cx| match signal {
|
||||
Signal::Event(id, event) => {
|
||||
this.handle_event(id, event, cx);
|
||||
}
|
||||
Signal::Eose(id) => {
|
||||
this.handle_eose(id, cx);
|
||||
}
|
||||
Signal::Closed(id, reason) => {
|
||||
this.handle_closed(id, reason, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user