tangled
alpha
login
or
join now
smokesignal.events
/
smokesignal
51
fork
atom
The smokesignal.events web application
51
fork
atom
overview
issues
7
pulls
pipelines
refactor: removed email sharing
Nick Gerakines
4 months ago
c88c2445
5513454d
+19
-97
9 changed files
expand all
collapse all
unified
split
src
http
handle_create_rsvp.rs
handle_export_rsvps.rs
handle_view_event.rs
import_utils.rs
rsvp_form.rs
processor.rs
storage
event.rs
templates
en-us
create_rsvp.partial.html
view_event.common.html
+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
48
-
// Check if user has email address set
49
49
-
let identity_has_email = current_handle
50
50
-
.email
51
51
-
.as_ref()
52
52
-
.is_some_and(|value| !value.is_empty());
53
53
-
54
48
let default_context = template_context! {
55
49
current_handle,
56
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
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
409
-
// Prepare context with email if shared
410
410
-
let mut context = serde_json::json!({});
411
411
-
if build_rsvp_form.email_shared.unwrap_or(false) && identity_has_email {
412
412
-
if let Some(email) = ¤t_handle.email {
413
413
-
context["email"] = serde_json::json!(email);
414
414
-
}
415
415
-
}
401
401
+
// Prepare context (empty - email sharing removed)
402
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
18
-
csv.push_str("event,rsvp,did,handle,status,created_at,email\n");
18
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
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
37
-
let email = escape_csv_field(&email_str);
38
36
39
37
csv.push_str(&format!(
40
40
-
"{},{},{},{},{},{},{}\n",
41
41
-
event, rsvp_aturi, did, handle, status, created_at, email
38
38
+
"{},{},{},{},{},{}\n",
39
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
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
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
188
-
assert_eq!(lines[0], "event,rsvp,did,handle,status,created_at,email");
189
189
-
assert!(lines[1].contains("user1@example.com"));
184
184
+
assert_eq!(lines[0], "event,rsvp,did,handle,status,created_at");
185
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
161
-
let identity_has_email = ctx
162
162
-
.current_handle
163
163
-
.as_ref()
164
164
-
.is_some_and(|handle| handle.email.as_ref().is_some_and(|value| !value.is_empty()));
165
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
290
-
// Get user's RSVP status and email sharing preference if logged in
291
291
-
let (user_rsvp_status, user_email_shared) = if let Some(current_entity) = &ctx.current_handle {
285
285
+
// Get user's RSVP status if logged in
286
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, ¤t_entity.did)
293
288
.await
294
289
{
295
295
-
Ok(Some((status, email_shared))) => (Some(status), email_shared),
296
296
-
Ok(None) => (None, false),
290
290
+
Ok(Some((status, _email_shared))) => Some(status),
291
291
+
Ok(None) => None,
297
292
Err(err) => {
298
293
tracing::error!("Error getting user RSVP status: {:?}", err);
299
299
-
(None, false)
294
294
+
None
300
295
}
301
296
}
302
297
} else {
303
303
-
(None, false)
298
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
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
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
149
-
email_shared: false,
150
149
},
151
150
)
152
151
.await
+1
-24
src/http/rsvp_form.rs
···
1
1
-
use serde::{Deserialize, Deserializer, Serialize};
1
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
9
-
#[allow(dead_code)]
10
10
-
fn deserialize_checkbox<'de, D>(deserializer: D) -> Result<bool, D::Error>
11
11
-
where
12
12
-
D: Deserializer<'de>,
13
13
-
{
14
14
-
let s = String::deserialize(deserializer)?.to_lowercase();
15
15
-
Ok(s == "true" || s == "ok" || s == "on")
16
16
-
}
17
17
-
18
18
-
fn deserialize_optional_checkbox<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
19
19
-
where
20
20
-
D: Deserializer<'de>,
21
21
-
{
22
22
-
let maybe_value: Option<String> = Option::deserialize(deserializer)?;
23
23
-
Ok(maybe_value.map(|value| {
24
24
-
let lower = value.to_lowercase();
25
25
-
lower == "true" || lower == "ok" || lower == "on"
26
26
-
}))
27
27
-
}
28
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
50
-
51
51
-
#[serde(default, deserialize_with = "deserialize_optional_checkbox")]
52
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
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
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
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
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
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
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
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
1105
-
SELECT
1101
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
1111
-
r.updated_at,
1112
1112
-
CASE
1113
1113
-
WHEN r.email_shared = true THEN ip.email
1114
1114
-
ELSE NULL
1115
1115
-
END as email
1107
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
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
1146
-
|(event_aturi, rsvp_aturi, did, handle, status, created_at, email)| RsvpExportData {
1137
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
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
91
-
{% if identity_has_email %}
92
92
-
<div class="field">
93
93
-
<label class="checkbox">
94
94
-
<input type="checkbox" name="email_shared" {% if build_rsvp_form.email_shared %} checked {% endif %}>
95
95
-
Privately share my email address with the event organizer
96
96
-
</label>
97
97
-
</div>
98
98
-
{% endif %}
99
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
219
-
{% if identity_has_email %}
220
220
-
<div class="field">
221
221
-
<div class="control is-expanded">
222
222
-
<label class="checkbox is-fullwidth">
223
223
-
<input type="checkbox" id="email_shared_checkbox" name="email_shared"{% if user_email_shared %} checked{% endif %}>
224
224
-
Privately share my email address with the event organizer
225
225
-
</label>
226
226
-
</div>
227
227
-
</div>
228
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
236
-
hx-include="#email_shared_checkbox, #rsvp_status">
226
226
+
hx-include="#rsvp_status">
237
227
RSVP
238
228
</button>
239
229
</div>