aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 1 | use std::{path::PathBuf, sync::Arc}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 2 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 3 | use axum::{ |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 4 | Router, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 5 | body::Bytes, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 6 | extract::{DefaultBodyLimit, State}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 7 | http::{HeaderMap, HeaderValue, StatusCode, header}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 8 | response::{IntoResponse, Response}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 9 | routing::{get, post}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 10 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 11 | use macltok_push_gateway::{GatewayDevice, GatewayNotification, notify_response, parse_notify}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 12 | use malice_push::{Content, Message, Notice, Outcome, Sender, Subscription, Urgency}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 13 | use serde_json::Value; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 14 | use tokio::sync::Semaphore; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 15 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 16 | pub const DEVICE_APP_ID: &str = "dev.malice.device"; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 17 | pub const NOTIFY_PATH: &str = "/_matrix/push/v1/notify"; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 18 | const NOTIFY_BODY_LIMIT: usize = 64 * 1024; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 19 | const CONCURRENT_NOTIFIES: usize = 16; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 20 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 21 | /// A push gateway that keeps nothing per user: each pusher's data carries its |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 22 | /// browser subscription's endpoint and auth secret, and its pushkey is the |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 23 | /// subscription's p256dh key, as Sygnal's webpush pushkin does it. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 24 | pub struct Gateway { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 25 | sender: Sender, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 26 | public_url: Option<String>, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 27 | permits: Semaphore, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 28 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 29 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 30 | impl Gateway { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 31 | pub fn new(sender: Sender, public_url: Option<String>) -> Self { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 32 | Self { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 33 | sender, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 34 | public_url, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 35 | permits: Semaphore::new(CONCURRENT_NOTIFIES), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 36 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 37 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 38 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 39 | /// Keeps the VAPID key in `MALICE_DATA_DIR`, systemd's `STATE_DIRECTORY` |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 40 | /// or the XDG state dir. Push stays off when none of them is usable. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 41 | pub fn from_env() -> Option<Self> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 42 | let Some(dir) = state_dir() else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 43 | tracing::warn!("no state directory for the VAPID key, push is off"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 44 | return None; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 45 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 46 | if let Err(error) = create_private_dir(&dir) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 47 | tracing::warn!(%error, dir = %dir.display(), "creating the push state directory failed, push is off"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 48 | return None; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 49 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 50 | let public_url = malice_push::env::public_url(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 51 | match malice_push::env::sender(&dir, public_url.as_deref()) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 52 | Ok(sender) => Some(Self::new(sender, public_url)), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 53 | Err(error) => { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 54 | tracing::warn!(%error, "push is off"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 55 | None |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 56 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 57 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 58 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 59 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 60 | fn origin(&self, headers: &HeaderMap) -> String { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 61 | if let Some(url) = &self.public_url { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 62 | return url.clone(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 63 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 64 | headers |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 65 | .get(header::HOST) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 66 | .and_then(|host| host.to_str().ok()) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 67 | .map(|host| format!("https://{host}")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 68 | .unwrap_or_default() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 69 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 70 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 71 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 72 | fn state_dir() -> Option<PathBuf> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 73 | let var = |name| std::env::var_os(name).filter(|value| !value.is_empty()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 74 | if let Some(dir) = var("MALICE_DATA_DIR") { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 75 | return Some(dir.into()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 76 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 77 | if let Some(dirs) = var("STATE_DIRECTORY") { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 78 | return std::env::split_paths(&dirs).next(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 79 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 80 | var("XDG_STATE_HOME") |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 81 | .map(PathBuf::from) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 82 | .or_else(|| var("HOME").map(|home| PathBuf::from(home).join(".local/state"))) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 83 | .map(|dir| dir.join("malice")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 84 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 85 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 86 | fn create_private_dir(path: &std::path::Path) -> std::io::Result<()> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 87 | let mut builder = std::fs::DirBuilder::new(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 88 | builder.recursive(true); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 89 | #[cfg(unix)] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 90 | { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 91 | use std::os::unix::fs::DirBuilderExt; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 92 | builder.mode(0o700); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 93 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 94 | builder.create(path) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 95 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 96 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 97 | pub fn push_router(gateway: Arc<Gateway>) -> Router { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 98 | Router::new() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 99 | .route("/push/vapid", get(vapid)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 100 | .route(NOTIFY_PATH, post(notify)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 101 | .layer(DefaultBodyLimit::max(NOTIFY_BODY_LIMIT)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 102 | .with_state(gateway) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 103 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 104 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 105 | /// Keeps the push paths from falling through to the app shell. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 106 | pub fn push_off_router() -> Router { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 107 | let off = || async { StatusCode::NOT_FOUND }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 108 | Router::new() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 109 | .route("/push/vapid", get(off)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 110 | .route(NOTIFY_PATH, post(off)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 111 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 112 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 113 | async fn vapid(State(gateway): State<Arc<Gateway>>) -> Response { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 114 | json_response( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 115 | StatusCode::OK, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 116 | serde_json::to_vec(&serde_json::json!({ "public_key": gateway.sender.vapid_public_key() })) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 117 | .unwrap_or_default(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 118 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 119 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 120 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 121 | pub fn json_response(status: StatusCode, body: Vec<u8>) -> Response { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 122 | ( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 123 | status, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 124 | [( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 125 | header::CONTENT_TYPE, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 126 | HeaderValue::from_static("application/json"), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 127 | )], |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 128 | body, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 129 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 130 | .into_response() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 131 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 132 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 133 | async fn notify(State(gateway): State<Arc<Gateway>>, headers: HeaderMap, body: Bytes) -> Response { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 134 | let notification = match parse_notify(&body) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 135 | Ok(notification) => notification, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 136 | Err(error) => { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 137 | return json_response( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 138 | StatusCode::BAD_REQUEST, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 139 | serde_json::to_vec(&serde_json::json!({ "error": error.to_string() })) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 140 | .unwrap_or_default(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 141 | ); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 142 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 143 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 144 | if !notification.is_event() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 145 | return json_response(StatusCode::OK, notify_response(&[])); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 146 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 147 | let Ok(_permit) = gateway.permits.acquire().await else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 148 | return StatusCode::SERVICE_UNAVAILABLE.into_response(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 149 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 150 | let origin = gateway.origin(&headers); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 151 | let mut rejected = Vec::new(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 152 | for device in ¬ification.devices { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 153 | if deliver(&gateway.sender, ¬ification, device, &origin) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 154 | .await |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 155 | .drops_subscription() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 156 | { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 157 | rejected.push(device.pushkey.clone()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 158 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 159 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 160 | json_response(StatusCode::OK, notify_response(&rejected)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 161 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 162 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 163 | async fn deliver( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 164 | sender: &Sender, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 165 | notification: &GatewayNotification, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 166 | device: &GatewayDevice, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 167 | origin: &str, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 168 | ) -> Outcome { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 169 | let data = |key: &str| device.data.get(key).and_then(Value::as_str); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 170 | let (DEVICE_APP_ID, Some(endpoint), Some(auth)) = |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 171 | (device.app_id.as_str(), data("endpoint"), data("auth")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 172 | else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 173 | return Outcome::Refused; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 174 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 175 | let subscription = Subscription { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 176 | endpoint: endpoint.to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 177 | p256dh: device.pushkey.clone(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 178 | auth: auth.to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 179 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 180 | let notice = device_notice(notification, device); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 181 | let urgency = if notice.mention || notification.room_name.is_none() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 182 | Urgency::High |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 183 | } else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 184 | Urgency::Normal |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 185 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 186 | let outcome = sender |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 187 | .send( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 188 | &subscription, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 189 | &Message { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 190 | payload: ¬ice.to_bytes(origin), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 191 | urgency, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 192 | topic: ¬ice.room_id, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 193 | }, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 194 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 195 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 196 | if matches!(outcome, Outcome::Failed(_) | Outcome::Retryable) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 197 | tracing::warn!(?outcome, "web push delivery failed"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 198 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 199 | outcome |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 200 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 201 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 202 | /// Room and sender as the homeserver named them. A room without a name is |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 203 | /// taken for a DM, so its title is the sender. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 204 | pub fn device_notice(notification: &GatewayNotification, device: &GatewayDevice) -> Notice { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 205 | let sender = notification |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 206 | .sender_display_name |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 207 | .clone() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 208 | .filter(|name| !name.trim().is_empty()) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 209 | .or_else(|| { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 210 | notification |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 211 | .sender |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 212 | .as_ref() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 213 | .map(|user| user.localpart().to_owned()) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 214 | }) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 215 | .unwrap_or_default(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 216 | let content: Option<Value> = notification |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 217 | .content |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 218 | .as_ref() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 219 | .and_then(|raw| serde_json::from_str(raw.get()).ok()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 220 | let content = Content::of( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 221 | notification.event_type.as_deref(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 222 | content.as_ref(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 223 | notification.user_is_target, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 224 | ); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 225 | let direct = notification.room_name.is_none(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 226 | Notice { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 227 | title: notification |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 228 | .room_name |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 229 | .clone() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 230 | .unwrap_or_else(|| sender.clone()), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 231 | body: content.body(&sender, direct), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 232 | room_id: notification |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 233 | .room_id |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 234 | .as_ref() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 235 | .map(ToString::to_string) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 236 | .unwrap_or_default(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 237 | event_id: notification |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 238 | .event_id |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 239 | .as_ref() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 240 | .map(ToString::to_string) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 241 | .unwrap_or_default(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 242 | encrypted: content == Content::Encrypted, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 243 | mention: device.tweaks.get("highlight").and_then(Value::as_bool) == Some(true), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 244 | silent: !device.tweaks.contains_key("sound"), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 245 | unread: Some(notification.counts.unread), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 246 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 247 | } |