xmtp_mls_core/
notifications.rs1use crate::context::CoreContext;
4use serde::{Deserialize, Serialize};
5use xmtp_cryptography::secret::{Secret, Zeroizing};
6use xmtp_db::consent_record::ConsentState;
7use xmtp_db::{StorageError, StoredSecret, notifications::StoredNotification};
8use xmtp_proto::backend_v1::{self, register_request::Delivery};
9
10pub fn notification_settings_changed(context: &impl CoreContext) {
13 context.task_channels().mark_notification_changed();
14 xmtp_events::EventWriter::emit(
15 context.events(),
16 None,
17 Some(crate::events::InternalEvent::NotificationSettingsChanged),
18 );
19}
20
21#[derive(Clone, Serialize, Deserialize)]
26pub enum NotificationChannel {
27 Apns { token: String },
28 Fcm { token: String },
29 Http { url: String, signing_key: Secret },
30}
31
32impl std::fmt::Debug for NotificationChannel {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.write_str(match self {
35 Self::Apns { .. } => "Apns",
36 Self::Fcm { .. } => "Fcm",
37 Self::Http { .. } => "Http",
38 })
39 }
40}
41
42#[derive(Clone, Serialize, Deserialize)]
44pub struct NotificationConfig {
45 pub channel: NotificationChannel,
46 pub consent_states: Vec<ConsentState>,
47 pub include_welcomes: bool,
48 pub include_sync_groups: bool,
49 pub include_commits: bool,
50}
51
52impl NotificationConfig {
53 pub fn new(channel: NotificationChannel) -> Self {
55 Self {
56 channel,
57 consent_states: vec![ConsentState::Allowed],
58 include_welcomes: true,
59 include_sync_groups: false,
60 include_commits: false,
61 }
62 }
63
64 pub fn channel_id(&self) -> i32 {
66 match self.channel {
67 NotificationChannel::Apns { .. } => backend_v1::Channel::Apns as i32,
68 NotificationChannel::Fcm { .. } => backend_v1::Channel::Fcm as i32,
69 NotificationChannel::Http { .. } => backend_v1::Channel::Http as i32,
70 }
71 }
72
73 pub fn registration(&self, record: &StoredNotification) -> backend_v1::RegisterRequest {
76 let delivery = match &self.channel {
77 NotificationChannel::Apns { token } => Delivery::Apns(backend_v1::ApnsDelivery {
78 token: token.clone(),
79 }),
80 NotificationChannel::Fcm { token } => Delivery::Fcm(backend_v1::FcmDelivery {
81 token: token.clone(),
82 }),
83 NotificationChannel::Http { url, signing_key } => {
84 Delivery::Http(backend_v1::HttpDelivery {
85 url: url.clone(),
86 signing_key: signing_key.as_slice().to_vec(),
87 })
88 }
89 };
90 backend_v1::RegisterRequest {
91 recipient_id: record.push_recipient_id.clone().unwrap_or_default(),
92 recipient_secret: recipient_secret(record),
93 delivery: Some(delivery),
94 }
95 }
96}
97
98pub fn recipient_secret(record: &StoredNotification) -> Vec<u8> {
100 record
101 .push_recipient_secret
102 .as_ref()
103 .map(|secret| secret.as_slice().to_vec())
104 .unwrap_or_default()
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub enum NotificationOverride {
110 Enabled,
111 Disabled,
112 Default,
113}
114
115pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, StorageError> {
117 serde_json::to_vec(value).map_err(|_| StorageError::DbSerialize)
118}
119
120pub fn decode<T: serde::de::DeserializeOwned>(value: &[u8]) -> Result<T, StorageError> {
122 serde_json::from_slice(value).map_err(|_| StorageError::DbDeserialize)
123}
124
125pub fn encode_config(config: &NotificationConfig) -> Result<StoredSecret, StorageError> {
130 struct Measure(usize);
131 impl std::io::Write for Measure {
132 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
133 self.0 += bytes.len();
134 Ok(bytes.len())
135 }
136 fn flush(&mut self) -> std::io::Result<()> {
137 Ok(())
138 }
139 }
140 let mut measure = Measure(0);
141 serde_json::to_writer(&mut measure, config).map_err(|_| StorageError::DbSerialize)?;
142 let mut bytes = Zeroizing::new(Vec::with_capacity(measure.0));
143 serde_json::to_writer(&mut *bytes, config).map_err(|_| StorageError::DbSerialize)?;
144 Ok(bytes.into())
145}
146
147pub fn stored_config(record: &StoredNotification) -> Result<NotificationConfig, StorageError> {
150 decode(
151 record
152 .push_config
153 .as_ref()
154 .ok_or(StorageError::DbDeserialize)?
155 .as_slice(),
156 )
157}
158
159pub fn effective(
161 config: &NotificationConfig,
162 group: &xmtp_db::group::StoredGroup,
163 consent: ConsentState,
164) -> bool {
165 use xmtp_proto::types::ConversationType;
166 if group.conversation_type == ConversationType::Sync {
167 return config.include_sync_groups;
168 }
169 if !matches!(
170 group.conversation_type,
171 ConversationType::Group | ConversationType::Dm
172 ) {
173 return false;
174 }
175 match group.push_override {
176 Some(1) => true,
177 Some(0) => false,
178 _ => config.consent_states.contains(&consent),
179 }
180}
181
182pub fn enabled_from_record(
185 record: &StoredNotification,
186 group: &xmtp_db::group::StoredGroup,
187 consent: ConsentState,
188) -> Result<bool, StorageError> {
189 if record.push_state != 1 {
190 return Ok(false);
191 }
192 let config = stored_config(record)?;
193 Ok(effective(&config, group, consent))
194}