···1+-- Create private_event_content table for conditional event content
2+CREATE TABLE private_event_content (
3+ aturi TEXT PRIMARY KEY,
4+ display_criteria JSONB NOT NULL DEFAULT '[]',
5+ content TEXT NOT NULL DEFAULT '',
6+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
7+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
8+);
9+10+-- Create index for faster lookups
11+CREATE INDEX idx_private_event_content_aturi ON private_event_content(aturi);
+1-1
src/bin/crypto.rs
···4use rand::RngCore;
56fn main() {
7- let mut rng = rand::thread_rng();
89 env::args().for_each(|arg| {
10 if arg.as_str() == "key" {
···4use rand::RngCore;
56fn main() {
7+ let mut rng = rand::rng();
89 env::args().for_each(|arg| {
10 if arg.as_str() == "key" {
+3-1
src/http/errors/create_rsvp_errors.rs
···17 ///
18 /// This error occurs when a user attempts to RSVP to an event that requires
19 /// confirmed email addresses, but the user has not confirmed their email address.
20- #[error("error-smokesignal-create-rsvp-2 Email confirmation required: This event requires a confirmed email address to RSVP. Please confirm your email address in your settings.")]
0021 EmailConfirmationRequired,
22}
···17 ///
18 /// This error occurs when a user attempts to RSVP to an event that requires
19 /// confirmed email addresses, but the user has not confirmed their email address.
20+ #[error(
21+ "error-smokesignal-create-rsvp-2 Email confirmation required: This event requires a confirmed email address to RSVP. Please confirm your email address in your settings."
22+ )]
23 EmailConfirmationRequired,
24}
···118 if !found_errors {
119 // Check if the event requires confirmed email
120 let event_aturi = build_rsvp_form.subject_aturi.as_ref().unwrap();
121- let event = crate::storage::event::event_get(&web_context.pool, event_aturi).await?;
0122123 if event.require_confirmed_email {
124 // Check if user has confirmed email
···118 if !found_errors {
119 // Check if the event requires confirmed email
120 let event_aturi = build_rsvp_form.subject_aturi.as_ref().unwrap();
121+ let event =
122+ crate::storage::event::event_get(&web_context.pool, event_aturi).await?;
123124 if event.require_confirmed_email {
125 // Check if user has confirmed email
+76-1
src/http/handle_edit_event.rs
···198 build_event_form.description = Some(description.clone());
199 build_event_form.require_confirmed_email = Some(event.require_confirmed_email);
2000000000000000000000000000201 // If we have a single address location, populate the form fields with its data
202 if let LocationEditStatus::Editable(Address {
203 country,
···650 error_template,
651 default_context,
652 err,
653- StatusCode::OK
654 );
00000000000000000000000000000000000000000000000000655 }
656657 // Send email notifications to RSVP holders if checkbox is checked and emailer is enabled
···198 build_event_form.description = Some(description.clone());
199 build_event_form.require_confirmed_email = Some(event.require_confirmed_email);
200201+ // Load private event content if it exists
202+ if let Ok(Some(private_content)) =
203+ crate::storage::private_event_content::private_event_content_get(
204+ &ctx.web_context.pool,
205+ &lookup_aturi,
206+ )
207+ .await
208+ {
209+ build_event_form.private_content = Some(private_content.content);
210+211+ // Set checkboxes based on criteria
212+ for criterion in private_content.display_criteria.0.iter() {
213+ match criterion.as_str() {
214+ "going_confirmed" => {
215+ build_event_form.private_content_criteria_going_confirmed = Some(true)
216+ }
217+ "going" => build_event_form.private_content_criteria_going = Some(true),
218+ "interested" => {
219+ build_event_form.private_content_criteria_interested = Some(true)
220+ }
221+ _ => {}
222+ }
223+ }
224+ }
225+226 // If we have a single address location, populate the form fields with its data
227 if let LocationEditStatus::Editable(Address {
228 country,
···675 error_template,
676 default_context,
677 err,
678+ StatusCode::INTERNAL_SERVER_ERROR
679 );
680+ }
681+682+ // Save private event content if provided
683+ let private_content = build_event_form.private_content.as_deref().unwrap_or("");
684+685+ // Build display criteria array from checkboxes
686+ let mut display_criteria = Vec::new();
687+ if build_event_form
688+ .private_content_criteria_going_confirmed
689+ .unwrap_or(false)
690+ {
691+ display_criteria.push("going_confirmed".to_string());
692+ }
693+ if build_event_form
694+ .private_content_criteria_going
695+ .unwrap_or(false)
696+ {
697+ display_criteria.push("going".to_string());
698+ }
699+ if build_event_form
700+ .private_content_criteria_interested
701+ .unwrap_or(false)
702+ {
703+ display_criteria.push("interested".to_string());
704+ }
705+706+ // Only save if there's content or criteria, otherwise delete
707+ if !private_content.is_empty() || !display_criteria.is_empty() {
708+ if let Err(err) =
709+ crate::storage::private_event_content::private_event_content_upsert(
710+ &ctx.web_context.pool,
711+ &lookup_aturi,
712+ &display_criteria,
713+ private_content,
714+ )
715+ .await
716+ {
717+ tracing::error!("Failed to save private event content: {:?}", err);
718+ }
719+ } else {
720+ // Delete private content if both content and criteria are empty
721+ if let Err(err) =
722+ crate::storage::private_event_content::private_event_content_delete(
723+ &ctx.web_context.pool,
724+ &lookup_aturi,
725+ )
726+ .await
727+ {
728+ tracing::error!("Failed to delete private event content: {:?}", err);
729+ }
730 }
731732 // Send email notifications to RSVP holders if checkbox is checked and emailer is enabled
···28use crate::storage::event::event_exists;
29use crate::storage::event::event_get;
30use crate::storage::event::get_event_rsvps_with_validation;
31-use crate::storage::event::get_user_rsvp_with_email_shared;
32use crate::storage::identity_profile::handle_for_did;
33use crate::storage::identity_profile::handle_for_handle;
34use crate::storage::identity_profile::handles_by_did;
···285 .is_some_and(|current_entity| current_entity.did == profile.did);
286287 // Get user's RSVP status if logged in
288- let user_rsvp_status = if let Some(current_entity) = &ctx.current_handle {
289- match get_user_rsvp_with_email_shared(&ctx.web_context.pool, &aturi, ¤t_entity.did)
00000290 .await
291- {
292- Ok(Some((status, _email_shared))) => Some(status),
293- Ok(None) => None,
294- Err(err) => {
295- tracing::error!("Error getting user RSVP status: {:?}", err);
296- None
0297 }
298- }
299- } else {
300- None
301- };
302303 // Check if there's a pending acceptance ticket for the current user's RSVP
304 let pending_acceptance = if let Some(current_entity) = &ctx.current_handle {
···398 let interested_tab_url = format!("?tab=interested&collection={}", encoded_collection);
399 let notgoing_tab_url = format!("?tab=notgoing&collection={}", encoded_collection);
400000000000000000000000000000000000000000000000000000000000000401 Ok((
402 StatusCode::OK,
403 RenderHtml(
···424 notgoing_tab_url,
425 // Email confirmation status
426 user_has_confirmed_email,
00427 },
428 ),
429 )
···28use crate::storage::event::event_exists;
29use crate::storage::event::event_get;
30use crate::storage::event::get_event_rsvps_with_validation;
31+use crate::storage::event::get_user_rsvp_status_and_validation;
32use crate::storage::identity_profile::handle_for_did;
33use crate::storage::identity_profile::handle_for_handle;
34use crate::storage::identity_profile::handles_by_did;
···285 .is_some_and(|current_entity| current_entity.did == profile.did);
286287 // Get user's RSVP status if logged in
288+ let (user_rsvp_status, user_rsvp_is_validated) =
289+ if let Some(current_entity) = &ctx.current_handle {
290+ match get_user_rsvp_status_and_validation(
291+ &ctx.web_context.pool,
292+ &aturi,
293+ ¤t_entity.did,
294+ )
295 .await
296+ {
297+ Ok(Some((status, is_validated))) => (Some(status), is_validated),
298+ Ok(None) => (None, false),
299+ Err(err) => {
300+ tracing::error!("Error getting user RSVP status: {:?}", err);
301+ (None, false)
302+ }
303 }
304+ } else {
305+ (None, false)
306+ };
0307308 // Check if there's a pending acceptance ticket for the current user's RSVP
309 let pending_acceptance = if let Some(current_entity) = &ctx.current_handle {
···403 let interested_tab_url = format!("?tab=interested&collection={}", encoded_collection);
404 let notgoing_tab_url = format!("?tab=notgoing&collection={}", encoded_collection);
405406+ // Check if private content should be displayed
407+ let private_content = if ctx.current_handle.is_some() {
408+ // Load private event content
409+ if let Ok(Some(private_event_content)) =
410+ crate::storage::private_event_content::private_event_content_get(
411+ &ctx.web_context.pool,
412+ &aturi,
413+ )
414+ .await
415+ {
416+ // Check if viewer meets any of the display criteria
417+ let mut meets_criteria = false;
418+419+ // If display_criteria is empty, show to anyone who has RSVP'd
420+ if private_event_content.display_criteria.0.is_empty() {
421+ meets_criteria = user_rsvp_status.is_some();
422+ } else {
423+ // Check specific criteria
424+ for criterion in private_event_content.display_criteria.0.iter() {
425+ match criterion.as_str() {
426+ "going_confirmed" => {
427+ // Check if user has "going" status AND has a validated RSVP
428+ if user_rsvp_status.as_deref() == Some("going")
429+ && user_rsvp_is_validated
430+ {
431+ meets_criteria = true;
432+ break;
433+ }
434+ }
435+ "going" => {
436+ // Check if user has "going" status
437+ if user_rsvp_status.as_deref() == Some("going") {
438+ meets_criteria = true;
439+ break;
440+ }
441+ }
442+ "interested" => {
443+ // Check if user has "interested" status
444+ if user_rsvp_status.as_deref() == Some("interested") {
445+ meets_criteria = true;
446+ break;
447+ }
448+ }
449+ _ => {}
450+ }
451+ }
452+ }
453+454+ if meets_criteria {
455+ Some(private_event_content.content)
456+ } else {
457+ None
458+ }
459+ } else {
460+ None
461+ }
462+ } else {
463+ None
464+ };
465+466 Ok((
467 StatusCode::OK,
468 RenderHtml(
···489 notgoing_tab_url,
490 // Email confirmation status
491 user_has_confirmed_email,
492+ // Private event content
493+ private_content,
494 },
495 ),
496 )
+3-2
src/storage/content.rs
···319 bloom_filter_capacity: usize,
320 bloom_filter_error_rate: f64,
321 ) -> Self {
322- let bloom_filter = Bloom::new_for_fp_rate(bloom_filter_capacity, bloom_filter_error_rate);
0323324 Self {
325 underlying_storage,
···381 // This is a limitation of bloom filters - in production you might want
382 // to use a counting bloom filter or periodically reset the filter
383 let mut filter = self.not_found_filter.write().await;
384- *filter = Bloom::new_for_fp_rate(10000, 0.01);
385 }
386}
387
···319 bloom_filter_capacity: usize,
320 bloom_filter_error_rate: f64,
321 ) -> Self {
322+ let bloom_filter = Bloom::new_for_fp_rate(bloom_filter_capacity, bloom_filter_error_rate)
323+ .expect("TODO fix this");
324325 Self {
326 underlying_storage,
···382 // This is a limitation of bloom filters - in production you might want
383 // to use a counting bloom filter or periodically reset the filter
384 let mut filter = self.not_found_filter.write().await;
385+ *filter = Bloom::new_for_fp_rate(10000, 0.01).expect("TODO fix this");
386 }
387}
388
+2-2
src/storage/event.rs
···589 Ok(status)
590}
591592-pub async fn get_user_rsvp_with_email_shared(
593 pool: &StoragePool,
594 event_aturi: &str,
595 did: &str,
···614 .map_err(StorageError::CannotBeginDatabaseTransaction)?;
615616 let result = sqlx::query_as::<_, (String, bool)>(
617- "SELECT status, email_shared FROM rsvps WHERE event_aturi = $1 AND did = $2",
618 )
619 .bind(event_aturi)
620 .bind(did)
···589 Ok(status)
590}
591592+pub async fn get_user_rsvp_status_and_validation(
593 pool: &StoragePool,
594 event_aturi: &str,
595 did: &str,
···614 .map_err(StorageError::CannotBeginDatabaseTransaction)?;
615616 let result = sqlx::query_as::<_, (String, bool)>(
617+ "SELECT status, validated_at IS NOT NULL FROM rsvps WHERE event_aturi = $1 AND did = $2",
618 )
619 .bind(event_aturi)
620 .bind(did)
+1
src/storage/mod.rs
···9pub mod identity_profile;
10pub mod notification;
11pub mod oauth;
012pub mod profile;
13pub mod types;
14pub mod webhook;
···9pub mod identity_profile;
10pub mod notification;
11pub mod oauth;
12+pub mod private_event_content;
13pub mod profile;
14pub mod types;
15pub mod webhook;