Skip to main content

iris_chat_protocol/protocol_engine/
engine_group_wakes.rs

1use super::*;
2
3pub(super) const MAX_READY_GROUP_PUBLICATIONS: usize = 4_096;
4
5impl ProtocolEngine {
6    fn ready_group_publication_is_allowed(&self, pending: &ProtocolReadyGroupPublish) -> bool {
7        // Envelopes sealed before carriers were recorded still name their
8        // recipient device. They are retired here, never offered again.
9        pending.publish.is_publishable()
10            && !pending
11                .publish
12                .chat_id
13                .strip_prefix("group:")
14                .is_some_and(|group| self.local_owner_is_inactive_for_group(group))
15            && !pending
16                .wake_recipient
17                .is_some_and(|owner| self.held_direct_chats.contains(&owner.to_hex()))
18    }
19
20    pub(super) fn retire_ineligible_group_publications(&mut self) -> anyhow::Result<()> {
21        let rejected = self
22            .ready_group_publications
23            .iter()
24            .filter(|(_, pending)| !self.ready_group_publication_is_allowed(pending))
25            .map(|(id, _)| id.clone())
26            .collect::<Vec<_>>();
27        if rejected.is_empty() {
28            return Ok(());
29        }
30        self.with_state_checkpoint(|engine| {
31            for id in rejected {
32                engine.ready_group_publications.remove(&id);
33            }
34            engine.persist_now()
35        })
36    }
37
38    pub(super) fn retain_group_publications(
39        &mut self,
40        effects: &[ProtocolEffect],
41        wake_recipient: Option<NdrOwnerPubkey>,
42    ) -> anyhow::Result<()> {
43        let added = effects
44            .iter()
45            .filter(|effect| {
46                let ProtocolEffect::Publish(publish) = effect;
47                !self
48                    .ready_group_publications
49                    .contains_key(&publish.event.id.to_hex())
50            })
51            .count();
52        anyhow::ensure!(
53            self.ready_group_publications.len().saturating_add(added)
54                <= MAX_READY_GROUP_PUBLICATIONS,
55            "Outgoing group queue is full"
56        );
57        for effect in effects {
58            let ProtocolEffect::Publish(publish) = effect;
59            self.ready_group_publications
60                .entry(publish.event.id.to_hex())
61                .or_insert_with(|| ProtocolReadyGroupPublish {
62                    publish: publish.clone(),
63                    wake_recipient,
64                });
65        }
66        Ok(())
67    }
68
69    /// Ready group ciphertext remains here until the application's durable
70    /// outbox owns it. Reoffering the same signed events never advances ratchets.
71    pub fn pending_group_publish_effects(&self) -> Vec<ProtocolEffect> {
72        self.ready_group_publications
73            .values()
74            .filter(|pending| self.ready_group_publication_is_allowed(pending))
75            .map(|pending| ProtocolEffect::Publish(pending.publish.clone()))
76            .collect()
77    }
78
79    pub fn has_pending_group_publish(&self, event_id: &str) -> bool {
80        self.ready_group_publications.contains_key(event_id)
81    }
82
83    /// Call only after the complete publication batch is committed to the
84    /// application outbox (or durably rejected by its block/removal policy).
85    pub fn ack_group_publish_effects(&mut self, effects: &[ProtocolEffect]) -> anyhow::Result<()> {
86        if !effects.iter().any(|effect| {
87            let ProtocolEffect::Publish(publish) = effect;
88            self.has_pending_group_publish(&publish.event.id.to_hex())
89        }) {
90            return Ok(());
91        }
92        self.with_state_checkpoint(|engine| {
93            for effect in effects {
94                let ProtocolEffect::Publish(publish) = effect;
95                engine
96                    .ready_group_publications
97                    .remove(&publish.event.id.to_hex());
98            }
99            engine.persist_now()
100        })
101    }
102
103    /// Prepare recipient-specific notification delivery in the same checkpoint
104    /// as the shared group ciphertext. Shared authors never enter push filters.
105    pub(super) fn prepare_private_group_wakes(
106        &mut self,
107        group_id: &str,
108        output: &mut ProtocolGroupSendResult,
109        inner_event_id: Option<String>,
110    ) -> anyhow::Result<()> {
111        let group = self
112            .group_manager
113            .group(group_id)
114            .ok_or_else(|| anyhow::anyhow!("group is unavailable"))?;
115        let own_authors = self.group_sender_event_pubkeys_for_owner(self.owner_pubkey);
116        let broadcasts = output
117            .effects
118            .iter()
119            .filter_map(|effect| {
120                let ProtocolEffect::Publish(publish) = effect;
121                (own_authors.contains(&publish.event.pubkey)
122                    && publish.carrier == ProtocolCarrier::SharedGroup)
123                    .then_some(publish.event.clone())
124            })
125            .collect::<Vec<_>>();
126        let chat_id = group_chat_id(group_id);
127        for event in broadcasts {
128            let content = serde_json::json!({
129                "v": 1, "type": "group-wake", "event": event,
130            })
131            .to_string();
132            let mut rumor = UnsignedEvent::new(
133                self.owner_pubkey,
134                event.created_at,
135                nostr::Kind::Custom(21_113),
136                [],
137                content,
138            );
139            rumor.ensure_id();
140            // A wake without an application message ID is still an individual
141            // delivery, not replaceable group metadata.
142            let delivery_id = inner_event_id
143                .clone()
144                .or_else(|| rumor.id.map(|id| id.to_hex()));
145            let payload = serde_json::to_vec(&rumor)?;
146            let mut rng = OsRng;
147            let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
148            for recipient_owner in group.members.iter().copied().collect::<BTreeSet<_>>() {
149                if recipient_owner == self.local_owner
150                    || self
151                        .held_direct_chats
152                        .contains(&public_owner(recipient_owner)?.to_hex())
153                {
154                    continue;
155                }
156                let prepared = self.session_manager.prepare_remote_send(
157                    &mut ctx,
158                    recipient_owner,
159                    payload.clone(),
160                )?;
161                let prepared = group_publish_from_prepared_send(
162                    prepared,
163                    GroupPendingFanout::Remote {
164                        recipient_owner,
165                        payload: payload.clone(),
166                    },
167                );
168                self.queue_group_pending_fanouts(group_id, &prepared, delivery_id.clone());
169                let ready = protocol_effects_from_group_prepared_publish(
170                    &prepared,
171                    self,
172                    delivery_id.clone(),
173                    chat_id.clone(),
174                    &mut output.event_ids,
175                )?;
176                self.retain_group_publications(&ready, Some(recipient_owner))?;
177                output.effects.extend(ready);
178            }
179            let payload = local_sibling_payload(self.owner_pubkey, &payload)?;
180            let prepared = self
181                .session_manager
182                .prepare_local_sibling_send_reusing_sessions(&mut ctx, payload.clone())?;
183            let prepared = group_publish_from_prepared_send(
184                prepared,
185                GroupPendingFanout::LocalSiblings { payload },
186            );
187            self.queue_group_pending_fanouts(group_id, &prepared, delivery_id.clone());
188            output
189                .effects
190                .extend(protocol_effects_from_group_prepared_publish(
191                    &prepared,
192                    self,
193                    delivery_id.clone(),
194                    chat_id.clone(),
195                    &mut output.event_ids,
196                )?);
197        }
198        Ok(())
199    }
200}