The smokesignal.events web application

refactor: removed email sharing

+19 -97
+2 -15
src/http/handle_create_rsvp.rs
··· 45 45 ) -> Result<impl IntoResponse, WebError> { 46 46 let current_handle = auth.require("/rsvp")?; 47 47 48 - // Check if user has email address set 49 - let identity_has_email = current_handle 50 - .email 51 - .as_ref() 52 - .is_some_and(|value| !value.is_empty()); 53 - 54 48 let default_context = template_context! { 55 49 current_handle, 56 - identity_has_email, 57 50 language => language.to_string(), 58 51 canonical_url => format!("https://{}/rsvp", web_context.config.external_base), 59 52 hx_request, ··· 224 217 event_aturi: build_rsvp_form.subject_aturi.as_ref().unwrap(), 225 218 event_cid: build_rsvp_form.subject_cid.as_ref().unwrap(), 226 219 status: build_rsvp_form.status.as_ref().unwrap(), 227 - email_shared: build_rsvp_form.email_shared.unwrap_or(false), 228 220 }, 229 221 ) 230 222 .await; ··· 406 398 if let Ok(webhooks) = 407 399 webhook_list_enabled_by_did(&web_context.pool, &webhook_identity).await 408 400 { 409 - // Prepare context with email if shared 410 - let mut context = serde_json::json!({}); 411 - if build_rsvp_form.email_shared.unwrap_or(false) && identity_has_email { 412 - if let Some(email) = &current_handle.email { 413 - context["email"] = serde_json::json!(email); 414 - } 415 - } 401 + // Prepare context (empty - email sharing removed) 402 + let context = serde_json::json!({}); 416 403 417 404 // Convert the RSVP record to JSON 418 405 let record_json = serde_json::json!({
+5 -9
src/http/handle_export_rsvps.rs
··· 15 15 let mut csv = String::new(); 16 16 17 17 // Add CSV header 18 - csv.push_str("event,rsvp,did,handle,status,created_at,email\n"); 18 + csv.push_str("event,rsvp,did,handle,status,created_at\n"); 19 19 20 20 // Add data rows 21 21 for rsvp in rsvps { ··· 25 25 }; 26 26 27 27 let handle_str = rsvp.handle.unwrap_or_default(); 28 - let email_str = rsvp.email.unwrap_or_default(); 29 28 30 29 // Escape CSV fields that might contain commas or quotes 31 30 let event = escape_csv_field(&rsvp.event_aturi); ··· 34 33 let handle = escape_csv_field(&handle_str); 35 34 let status = escape_csv_field(&rsvp.status); 36 35 let created_at = escape_csv_field(&created_at_str); 37 - let email = escape_csv_field(&email_str); 38 36 39 37 csv.push_str(&format!( 40 - "{},{},{},{},{},{},{}\n", 41 - event, rsvp_aturi, did, handle, status, created_at, email 38 + "{},{},{},{},{},{}\n", 39 + event, rsvp_aturi, did, handle, status, created_at 42 40 )); 43 41 } 44 42 ··· 168 166 handle: Some("user1.bsky.social".to_string()), 169 167 status: "going".to_string(), 170 168 created_at: Some(created_at), 171 - email: Some("user1@example.com".to_string()), 172 169 }, 173 170 RsvpExportData { 174 171 event_aturi: "at://did:example/collection/event1".to_string(), ··· 177 174 handle: None, 178 175 status: "interested".to_string(), 179 176 created_at: None, 180 - email: None, 181 177 }, 182 178 ]; 183 179 ··· 185 181 let lines: Vec<&str> = csv.lines().collect(); 186 182 187 183 assert_eq!(lines.len(), 3); // Header + 2 data rows 188 - assert_eq!(lines[0], "event,rsvp,did,handle,status,created_at,email"); 189 - assert!(lines[1].contains("user1@example.com")); 184 + assert_eq!(lines[0], "event,rsvp,did,handle,status,created_at"); 185 + assert!(lines[1].contains("user1.bsky.social")); 190 186 assert!(lines[2].contains("interested")); 191 187 } 192 188 }
+6 -13
src/http/handle_view_event.rs
··· 158 158 159 159 let profile = profile.unwrap(); 160 160 161 - let identity_has_email = ctx 162 - .current_handle 163 - .as_ref() 164 - .is_some_and(|handle| handle.email.as_ref().is_some_and(|value| !value.is_empty())); 165 - 166 161 // We'll use TimeZoneSelector to implement the time zone selection logic 167 162 // The timezone selection will happen after we fetch the event 168 163 ··· 287 282 .clone() 288 283 .is_some_and(|current_entity| current_entity.did == profile.did); 289 284 290 - // Get user's RSVP status and email sharing preference if logged in 291 - let (user_rsvp_status, user_email_shared) = if let Some(current_entity) = &ctx.current_handle { 285 + // Get user's RSVP status if logged in 286 + let user_rsvp_status = if let Some(current_entity) = &ctx.current_handle { 292 287 match get_user_rsvp_with_email_shared(&ctx.web_context.pool, &aturi, &current_entity.did) 293 288 .await 294 289 { 295 - Ok(Some((status, email_shared))) => (Some(status), email_shared), 296 - Ok(None) => (None, false), 290 + Ok(Some((status, _email_shared))) => Some(status), 291 + Ok(None) => None, 297 292 Err(err) => { 298 293 tracing::error!("Error getting user RSVP status: {:?}", err); 299 - (None, false) 294 + None 300 295 } 301 296 } 302 297 } else { 303 - (None, false) 298 + None 304 299 }; 305 300 306 301 // Get counts for all RSVP statuses ··· 366 361 template_context! { 367 362 current_handle => ctx.current_handle, 368 363 language => ctx.language.to_string(), 369 - identity_has_email, 370 364 canonical_url => event_url, 371 365 login_url => login_url, 372 366 event => event_with_counts, ··· 375 369 active_tab_handles, 376 370 active_tab => tab_name, 377 371 user_rsvp_status, 378 - user_email_shared, 379 372 handle_slug, 380 373 event_rkey, 381 374 collection => collection.clone(),
-1
src/http/import_utils.rs
··· 146 146 event_aturi: &event_aturi, 147 147 event_cid: &event_cid, 148 148 status, 149 - email_shared: false, 150 149 }, 151 150 ) 152 151 .await
+1 -24
src/http/rsvp_form.rs
··· 1 - use serde::{Deserialize, Deserializer, Serialize}; 1 + use serde::{Deserialize, Serialize}; 2 2 3 3 use crate::{ 4 4 errors::expand_error, ··· 6 6 storage::{StoragePool, event::event_get_cid}, 7 7 }; 8 8 9 - #[allow(dead_code)] 10 - fn deserialize_checkbox<'de, D>(deserializer: D) -> Result<bool, D::Error> 11 - where 12 - D: Deserializer<'de>, 13 - { 14 - let s = String::deserialize(deserializer)?.to_lowercase(); 15 - Ok(s == "true" || s == "ok" || s == "on") 16 - } 17 - 18 - fn deserialize_optional_checkbox<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error> 19 - where 20 - D: Deserializer<'de>, 21 - { 22 - let maybe_value: Option<String> = Option::deserialize(deserializer)?; 23 - Ok(maybe_value.map(|value| { 24 - let lower = value.to_lowercase(); 25 - lower == "true" || lower == "ok" || lower == "on" 26 - })) 27 - } 28 - 29 9 #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone)] 30 10 pub(crate) enum BuildRsvpContentState { 31 11 #[default] ··· 47 27 48 28 pub(crate) status: Option<String>, 49 29 pub(crate) status_error: Option<String>, 50 - 51 - #[serde(default, deserialize_with = "deserialize_optional_checkbox")] 52 - pub(crate) email_shared: Option<bool>, 53 30 } 54 31 55 32 impl BuildRSVPForm {
-1
src/processor.rs
··· 224 224 event_aturi: &event_aturi, 225 225 event_cid: &event_cid, 226 226 status, 227 - email_shared: false, 228 227 }, 229 228 ) 230 229 .await?;
+4 -14
src/storage/event.rs
··· 131 131 pub event_aturi: &'a str, 132 132 pub event_cid: &'a str, 133 133 pub status: &'a str, 134 - pub email_shared: bool, 135 134 } 136 135 137 136 pub async fn rsvp_insert_with_metadata<T: serde::Serialize>( ··· 146 145 let now = Utc::now(); 147 146 148 147 // TODO: This should probably also update event_aturi and event_cid 149 - sqlx::query("INSERT INTO rsvps (aturi, cid, did, lexicon, record, event_aturi, event_cid, status, email_shared, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (aturi) DO UPDATE SET record = $5, cid = $2, status = $8, email_shared = $9, updated_at = $10") 148 + sqlx::query("INSERT INTO rsvps (aturi, cid, did, lexicon, record, event_aturi, event_cid, status, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (aturi) DO UPDATE SET record = $5, cid = $2, status = $8, updated_at = $9") 150 149 .bind(params.aturi) 151 150 .bind(params.cid) 152 151 .bind(params.did) ··· 155 154 .bind(params.event_aturi) 156 155 .bind(params.event_cid) 157 156 .bind(params.status) 158 - .bind(params.email_shared) 159 157 .bind(now) 160 158 .execute(tx.as_mut()) 161 159 .await ··· 195 193 event_aturi: &event_aturi, 196 194 event_cid: &event_cid, 197 195 status, 198 - email_shared: false, 199 196 }, 200 197 ) 201 198 .await ··· 1081 1078 pub handle: Option<String>, 1082 1079 pub status: String, 1083 1080 pub created_at: Option<chrono::DateTime<chrono::Utc>>, 1084 - pub email: Option<String>, 1085 1081 } 1086 1082 1087 1083 /// Get all RSVPs for an event with detailed information for CSV export ··· 1102 1098 .map_err(StorageError::CannotBeginDatabaseTransaction)?; 1103 1099 1104 1100 let query = r#" 1105 - SELECT 1101 + SELECT 1106 1102 r.event_aturi, 1107 1103 r.aturi as rsvp_aturi, 1108 1104 r.did, 1109 1105 ip.handle, 1110 1106 r.status, 1111 - r.updated_at, 1112 - CASE 1113 - WHEN r.email_shared = true THEN ip.email 1114 - ELSE NULL 1115 - END as email 1107 + r.updated_at 1116 1108 FROM rsvps r 1117 1109 LEFT JOIN identity_profiles ip ON r.did = ip.did 1118 1110 WHERE r.event_aturi = $1 ··· 1128 1120 Option<String>, 1129 1121 String, 1130 1122 Option<chrono::DateTime<chrono::Utc>>, 1131 - Option<String>, 1132 1123 ), 1133 1124 >(query) 1134 1125 .bind(event_aturi) ··· 1143 1134 let export_data: Vec<RsvpExportData> = rsvps 1144 1135 .into_iter() 1145 1136 .map( 1146 - |(event_aturi, rsvp_aturi, did, handle, status, created_at, email)| RsvpExportData { 1137 + |(event_aturi, rsvp_aturi, did, handle, status, created_at)| RsvpExportData { 1147 1138 event_aturi, 1148 1139 rsvp_aturi, 1149 1140 did, 1150 1141 handle, 1151 1142 status, 1152 1143 created_at, 1153 - email, 1154 1144 }, 1155 1145 ) 1156 1146 .collect();
-9
templates/en-us/create_rsvp.partial.html
··· 88 88 {% endif %} 89 89 </div> 90 90 91 - {% if identity_has_email %} 92 - <div class="field"> 93 - <label class="checkbox"> 94 - <input type="checkbox" name="email_shared" {% if build_rsvp_form.email_shared %} checked {% endif %}> 95 - Privately share my email address with the event organizer 96 - </label> 97 - </div> 98 - {% endif %} 99 - 100 91 <hr/> 101 92 <div class="field"> 102 93 <div class="control">
+1 -11
templates/en-us/view_event.common.html
··· 216 216 </div> 217 217 </div> 218 218 </div> 219 - {% if identity_has_email %} 220 - <div class="field"> 221 - <div class="control is-expanded"> 222 - <label class="checkbox is-fullwidth"> 223 - <input type="checkbox" id="email_shared_checkbox" name="email_shared"{% if user_email_shared %} checked{% endif %}> 224 - Privately share my email address with the event organizer 225 - </label> 226 - </div> 227 - </div> 228 - {% endif %} 229 219 </div> 230 220 <div class="column"> 231 221 <div class="field"> ··· 233 223 <button class="button is-primary is-fullwidth" hx-post="/rsvp" hx-target="#rsvpFrame" 234 224 hx-swap="outerHTML" 235 225 hx-vals='{"subject_aturi": "{{ event.aturi }}", "build_state": "Review"}' 236 - hx-include="#email_shared_checkbox, #rsvp_status"> 226 + hx-include="#rsvp_status"> 237 227 RSVP 238 228 </button> 239 229 </div>