add group activity rows to nip29 timeline
This commit is contained in:
+153
-46
@@ -13,21 +13,51 @@ use state::NostrRegistry;
|
||||
|
||||
use crate::LOCAL_KEYS;
|
||||
use crate::protocol::{
|
||||
GroupAdmins, GroupId, GroupKey, GroupMembers, GroupMetadata, GroupParticipants, GroupPins,
|
||||
GroupRoles, LivekitSession, MEMBERSHIP_KINDS, Membership, PIN_LIST, Pin, RENDER_KINDS,
|
||||
STATE_KINDS, TimelineWindow,
|
||||
ACTIVITY_KINDS, Activity, GroupAdmins, GroupId, GroupKey, GroupMembers, GroupMetadata,
|
||||
GroupParticipants, GroupPins, GroupRoles, LivekitSession, MEMBERSHIP_KINDS, Membership,
|
||||
PIN_LIST, Pin, RENDER_KINDS, STATE_KINDS, TimelineWindow,
|
||||
};
|
||||
|
||||
const REPLAY_LIMIT: usize = 100;
|
||||
const PAGE_LIMIT: usize = 50;
|
||||
const PAGE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const LIVEKIT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const LEAVE_WINDOW_SECS: u64 = 5 * 60;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Absorbed {
|
||||
Ignored,
|
||||
Rendered { fresh: bool },
|
||||
Changed,
|
||||
Shown { fresh: bool, updated: bool },
|
||||
Updated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Row {
|
||||
Message(Box<Message>),
|
||||
Activity(Activity),
|
||||
}
|
||||
|
||||
impl Row {
|
||||
pub fn id(&self) -> EventId {
|
||||
match self {
|
||||
Self::Message(message) => message.id,
|
||||
Self::Activity(activity) => activity.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn author(&self) -> PublicKey {
|
||||
match self {
|
||||
Self::Message(message) => message.author,
|
||||
Self::Activity(activity) => activity.author,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> Timestamp {
|
||||
match self {
|
||||
Self::Message(message) => message.created_at,
|
||||
Self::Activity(activity) => activity.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -51,8 +81,9 @@ pub struct Group {
|
||||
membership_events: Vec<Event>,
|
||||
elsewhere: Option<GroupKey>,
|
||||
state_ids: HashMap<Kind, EventId>,
|
||||
messages: Vec<Message>,
|
||||
rows: Vec<Row>,
|
||||
index: HashSet<EventId>,
|
||||
leave_requests: HashMap<PublicKey, Timestamp>,
|
||||
window: TimelineWindow,
|
||||
oldest: Option<Timestamp>,
|
||||
loading: bool,
|
||||
@@ -77,8 +108,9 @@ impl Group {
|
||||
membership_events: Vec::new(),
|
||||
elsewhere: None,
|
||||
state_ids: HashMap::new(),
|
||||
messages: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
index: HashSet::new(),
|
||||
leave_requests: HashMap::new(),
|
||||
window: TimelineWindow::new(),
|
||||
oldest: None,
|
||||
loading: true,
|
||||
@@ -141,8 +173,15 @@ impl Group {
|
||||
self.elsewhere.as_ref()
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> &[Message] {
|
||||
&self.messages
|
||||
pub fn rows(&self) -> &[Row] {
|
||||
&self.rows
|
||||
}
|
||||
|
||||
pub fn message(&self, id: EventId) -> Option<&Message> {
|
||||
self.rows.iter().find_map(|row| match row {
|
||||
Row::Message(message) if message.id == id => Some(message.as_ref()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn loading(&self) -> bool {
|
||||
@@ -311,11 +350,13 @@ impl Group {
|
||||
log::warn!("nip29: connecting {relay} failed: {error}");
|
||||
}
|
||||
|
||||
let filters = vec![
|
||||
let filters = [
|
||||
key.id().timeline_filter(REPLAY_LIMIT),
|
||||
key.id().state_filter(),
|
||||
key.id().membership_filter(me),
|
||||
];
|
||||
]
|
||||
.into_iter()
|
||||
.chain(key.id().state_filters())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(error) = client
|
||||
.subscribe(ReqTarget::single(relay, filters))
|
||||
@@ -485,9 +526,19 @@ impl Group {
|
||||
|
||||
for event in &events {
|
||||
match self.absorb(event, me) {
|
||||
Absorbed::Rendered { fresh: true } => rendered.push(event.clone()),
|
||||
Absorbed::Changed => changed = true,
|
||||
_ => {}
|
||||
Absorbed::Shown {
|
||||
fresh: true,
|
||||
updated,
|
||||
} => {
|
||||
rendered.push(event.clone());
|
||||
changed |= updated;
|
||||
}
|
||||
Absorbed::Shown {
|
||||
fresh: false,
|
||||
updated,
|
||||
} => changed |= updated,
|
||||
Absorbed::Updated => changed = true,
|
||||
Absorbed::Ignored => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +560,7 @@ impl Group {
|
||||
return;
|
||||
}
|
||||
|
||||
self.messages.retain(|message| message.id != id);
|
||||
self.rows.retain(|row| row.id() != id);
|
||||
cx.emit(GroupEvent::Updated);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -530,29 +581,7 @@ impl Group {
|
||||
fn absorb(&mut self, event: &Event, me: PublicKey) -> Absorbed {
|
||||
if STATE_KINDS.contains(&event.kind) {
|
||||
return if self.absorb_state(event) {
|
||||
Absorbed::Changed
|
||||
} else {
|
||||
Absorbed::Ignored
|
||||
};
|
||||
}
|
||||
|
||||
if MEMBERSHIP_KINDS.contains(&event.kind) {
|
||||
self.track(event);
|
||||
|
||||
if !event.tags.public_keys().any(|key| key == me) {
|
||||
return Absorbed::Ignored;
|
||||
}
|
||||
|
||||
if !self
|
||||
.membership_events
|
||||
.iter()
|
||||
.any(|existing| existing.id == event.id)
|
||||
{
|
||||
self.membership_events.push(event.clone());
|
||||
}
|
||||
|
||||
return if self.refresh_membership(me) {
|
||||
Absorbed::Changed
|
||||
Absorbed::Updated
|
||||
} else {
|
||||
Absorbed::Ignored
|
||||
};
|
||||
@@ -560,15 +589,49 @@ impl Group {
|
||||
|
||||
self.track(event);
|
||||
|
||||
let updated = self.absorb_membership(event, me);
|
||||
|
||||
if ACTIVITY_KINDS.contains(&event.kind) {
|
||||
return Absorbed::Shown {
|
||||
fresh: self.push_activity(event),
|
||||
updated,
|
||||
};
|
||||
}
|
||||
|
||||
if RENDER_KINDS.contains(&event.kind) {
|
||||
Absorbed::Rendered {
|
||||
fresh: self.push_message(event),
|
||||
}
|
||||
return Absorbed::Shown {
|
||||
fresh: self.push_row(event),
|
||||
updated,
|
||||
};
|
||||
}
|
||||
|
||||
if updated {
|
||||
Absorbed::Updated
|
||||
} else {
|
||||
Absorbed::Ignored
|
||||
}
|
||||
}
|
||||
|
||||
fn absorb_membership(&mut self, event: &Event, me: PublicKey) -> bool {
|
||||
if !MEMBERSHIP_KINDS.contains(&event.kind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !event.tags.public_keys().any(|key| key == me) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self
|
||||
.membership_events
|
||||
.iter()
|
||||
.any(|existing| existing.id == event.id)
|
||||
{
|
||||
self.membership_events.push(event.clone());
|
||||
}
|
||||
|
||||
self.refresh_membership(me)
|
||||
}
|
||||
|
||||
fn track(&mut self, event: &Event) {
|
||||
self.window.push(event.clone());
|
||||
|
||||
@@ -609,17 +672,61 @@ impl Group {
|
||||
self.state_ids.insert(event.kind, event.id) != Some(event.id)
|
||||
}
|
||||
|
||||
fn push_message(&mut self, event: &Event) -> bool {
|
||||
fn push_row(&mut self, event: &Event) -> bool {
|
||||
if !self.index.insert(event.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let message = Message::from(event);
|
||||
let position = self
|
||||
.messages
|
||||
.partition_point(|existing| existing.created_at <= message.created_at);
|
||||
.rows
|
||||
.partition_point(|row| row.created_at() <= message.created_at);
|
||||
|
||||
self.messages.insert(position, message);
|
||||
self.rows.insert(position, Row::Message(Box::new(message)));
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn push_activity(&mut self, event: &Event) -> bool {
|
||||
let activity = match Activity::parse(event) {
|
||||
Ok(activity) => activity,
|
||||
Err(error) => {
|
||||
log::warn!("nip29: ignoring a malformed group activity: {error}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if activity.kind == Kind::GroupPutUser && activity.roles.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if activity.kind == Kind::GroupRemoveUser
|
||||
&& self
|
||||
.leave_requests
|
||||
.get(&activity.subject())
|
||||
.is_some_and(|left| {
|
||||
activity.created_at >= *left
|
||||
&& activity.created_at.as_secs().saturating_sub(left.as_secs())
|
||||
<= LEAVE_WINDOW_SECS
|
||||
})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.index.insert(activity.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if activity.kind == Kind::GroupLeaveRequest {
|
||||
self.leave_requests
|
||||
.insert(activity.subject(), activity.created_at);
|
||||
}
|
||||
|
||||
let position = self
|
||||
.rows
|
||||
.partition_point(|row| row.created_at() <= activity.created_at);
|
||||
|
||||
self.rows.insert(position, Row::Activity(activity));
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@ pub const STATE_KINDS: [Kind; 6] = [
|
||||
|
||||
pub const MEMBERSHIP_KINDS: [Kind; 2] = [Kind::GroupPutUser, Kind::GroupRemoveUser];
|
||||
|
||||
pub const ACTIVITY_KINDS: [Kind; 4] = [
|
||||
Kind::GroupPutUser,
|
||||
Kind::GroupRemoveUser,
|
||||
Kind::GroupJoinRequest,
|
||||
Kind::GroupLeaveRequest,
|
||||
];
|
||||
|
||||
fn has_tag(tags: &Tags, name: &str) -> bool {
|
||||
tags.iter().any(|tag| tag.kind() == name)
|
||||
}
|
||||
@@ -76,8 +83,11 @@ impl GroupId {
|
||||
self.timeline_filter(limit).until(until)
|
||||
}
|
||||
|
||||
pub fn state_filter(&self) -> Filter {
|
||||
Filter::new().identifier(self.0.clone()).kinds(STATE_KINDS)
|
||||
pub fn state_filters(&self) -> Vec<Filter> {
|
||||
STATE_KINDS
|
||||
.iter()
|
||||
.map(|kind| Filter::new().identifier(self.0.clone()).kind(*kind))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn membership_filter(&self, me: PublicKey) -> Filter {
|
||||
@@ -815,6 +825,57 @@ impl Membership {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Activity {
|
||||
pub id: EventId,
|
||||
pub kind: Kind,
|
||||
pub author: PublicKey,
|
||||
pub target: Option<PublicKey>,
|
||||
pub roles: Vec<String>,
|
||||
pub created_at: Timestamp,
|
||||
}
|
||||
|
||||
impl Activity {
|
||||
pub fn parse(event: &Event) -> Result<Self> {
|
||||
if !ACTIVITY_KINDS.contains(&event.kind) {
|
||||
bail!("not a group activity event: kind {}", event.kind);
|
||||
}
|
||||
|
||||
let mut fields = event
|
||||
.tags
|
||||
.iter()
|
||||
.find(|tag| tag.kind() == "p")
|
||||
.map(|tag| tag.as_slice().iter().skip(1))
|
||||
.into_iter()
|
||||
.flatten();
|
||||
|
||||
let target = match fields.next() {
|
||||
Some(value) => Some(PublicKey::from_hex(value)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
if target.is_none() && MEMBERSHIP_KINDS.contains(&event.kind) {
|
||||
bail!("a membership event has no p tag");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id: event.id,
|
||||
kind: event.kind,
|
||||
author: event.pubkey,
|
||||
target,
|
||||
roles: fields.cloned().collect(),
|
||||
created_at: event.created_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn subject(&self) -> PublicKey {
|
||||
match self.kind {
|
||||
Kind::GroupPutUser | Kind::GroupRemoveUser => self.target.unwrap_or(self.author),
|
||||
_ => self.author,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TimelineWindow(Vec<Event>);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user