tangled
alpha
login
or
join now
zzstoatzz.io
/
status
0
fork
atom
slack status without the slack
status.zzstoatzz.io/
quickslice
0
fork
atom
overview
issues
pulls
pipelines
fix warnings
zzstoatzz.io
6 months ago
32e49c12
b567348c
+179
-25
3 changed files
expand all
collapse all
unified
split
src
main.rs
templates.rs
templates
status.html
+106
src/main.rs
···
791
expires_in: Option<String>, // e.g., "1h", "30m", "1d", etc.
792
}
793
0
0
0
0
0
0
794
/// Parse duration string like "1h", "30m", "1d" into chrono::Duration
795
fn parse_duration(duration_str: &str) -> Option<chrono::Duration> {
796
if duration_str.is_empty() {
···
905
.see_other()
906
.respond_to(&request)
907
.map_into_boxed_body()
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
908
}
909
}
910
}
···
1195
.service(user_status_json)
1196
.service(status)
1197
.service(clear_status)
0
1198
})
1199
.bind((host.as_str(), port))?
1200
.run()
···
791
expires_in: Option<String>, // e.g., "1h", "30m", "1d", etc.
792
}
793
794
+
/// The post body for deleting a specific status
795
+
#[derive(Serialize, Deserialize)]
796
+
struct DeleteRequest {
797
+
uri: String,
798
+
}
799
+
800
/// Parse duration string like "1h", "30m", "1d" into chrono::Duration
801
fn parse_duration(duration_str: &str) -> Option<chrono::Duration> {
802
if duration_str.is_empty() {
···
911
.see_other()
912
.respond_to(&request)
913
.map_into_boxed_body()
914
+
}
915
+
}
916
+
}
917
+
918
+
/// Delete a specific status by URI (JSON endpoint)
919
+
#[post("/status/delete")]
920
+
async fn delete_status(
921
+
session: Session,
922
+
oauth_client: web::Data<OAuthClientType>,
923
+
db_pool: web::Data<Arc<Pool>>,
924
+
req: web::Json<DeleteRequest>,
925
+
) -> HttpResponse {
926
+
// Check if the user is logged in
927
+
match session.get::<String>("did").unwrap_or(None) {
928
+
Some(did_string) => {
929
+
let did = Did::new(did_string.clone()).expect("failed to parse did");
930
+
931
+
// Parse the URI to verify it belongs to this user
932
+
// URI format: at://did:plc:xxx/io.zzstoatzz.status.record/rkey
933
+
let uri_parts: Vec<&str> = req.uri.split('/').collect();
934
+
if uri_parts.len() < 5 {
935
+
return HttpResponse::BadRequest().json(serde_json::json!({
936
+
"error": "Invalid status URI format"
937
+
}));
938
+
}
939
+
940
+
// Extract DID from URI (at://did:plc:xxx/...)
941
+
let uri_did_part = uri_parts[2];
942
+
if uri_did_part != did_string {
943
+
return HttpResponse::Forbidden().json(serde_json::json!({
944
+
"error": "You can only delete your own statuses"
945
+
}));
946
+
}
947
+
948
+
// Extract record key
949
+
if let Some(rkey) = uri_parts.last() {
950
+
// Get OAuth session
951
+
match oauth_client.restore(&did).await {
952
+
Ok(session) => {
953
+
let agent = Agent::new(session);
954
+
955
+
// Delete the record from ATProto
956
+
let delete_request =
957
+
atrium_api::com::atproto::repo::delete_record::InputData {
958
+
collection: atrium_api::types::string::Nsid::new(
959
+
"io.zzstoatzz.status.record".to_string(),
960
+
)
961
+
.expect("valid nsid"),
962
+
repo: did.clone().into(),
963
+
rkey: atrium_api::types::string::RecordKey::new(
964
+
rkey.to_string(),
965
+
)
966
+
.expect("valid rkey"),
967
+
swap_commit: None,
968
+
swap_record: None,
969
+
};
970
+
971
+
match agent
972
+
.api
973
+
.com
974
+
.atproto
975
+
.repo
976
+
.delete_record(delete_request.into())
977
+
.await
978
+
{
979
+
Ok(_) => {
980
+
// Also remove from local database
981
+
let _ = StatusFromDb::delete_by_uri(&db_pool, req.uri.clone()).await;
982
+
983
+
HttpResponse::Ok().json(serde_json::json!({
984
+
"success": true
985
+
}))
986
+
}
987
+
Err(e) => {
988
+
log::error!("Failed to delete status from ATProto: {e}");
989
+
HttpResponse::InternalServerError().json(serde_json::json!({
990
+
"error": "Failed to delete status"
991
+
}))
992
+
}
993
+
}
994
+
}
995
+
Err(e) => {
996
+
log::error!("Failed to restore OAuth session: {e}");
997
+
HttpResponse::InternalServerError().json(serde_json::json!({
998
+
"error": "Session error"
999
+
}))
1000
+
}
1001
+
}
1002
+
} else {
1003
+
HttpResponse::BadRequest().json(serde_json::json!({
1004
+
"error": "Invalid status URI"
1005
+
}))
1006
+
}
1007
+
}
1008
+
None => {
1009
+
// Not logged in
1010
+
HttpResponse::Unauthorized().json(serde_json::json!({
1011
+
"error": "Not authenticated"
1012
+
}))
1013
}
1014
}
1015
}
···
1300
.service(user_status_json)
1301
.service(status)
1302
.service(clear_status)
1303
+
.service(delete_status)
1304
})
1305
.bind((host.as_str(), port))?
1306
.run()
+6
src/templates.rs
···
7
#[derive(Template)]
8
#[template(path = "home.html")]
9
pub struct HomeTemplate<'a> {
0
10
pub title: &'a str,
11
pub status_options: &'a [&'a str],
12
pub profile: Option<Profile>,
···
23
#[derive(Template)]
24
#[template(path = "login.html")]
25
pub struct LoginTemplate<'a> {
0
26
pub title: &'a str,
27
pub error: Option<&'a str>,
28
}
···
30
#[derive(Template)]
31
#[template(path = "error.html")]
32
pub struct ErrorTemplate<'a> {
0
33
pub title: &'a str,
34
pub error: &'a str,
35
}
···
37
#[derive(Template)]
38
#[template(path = "status.html")]
39
pub struct StatusTemplate<'a> {
0
40
pub title: &'a str,
41
pub handle: String,
0
42
pub status_options: &'a [&'a str],
43
pub current_status: Option<StatusFromDb>,
44
pub history: Vec<StatusFromDb>,
···
48
#[derive(Template)]
49
#[template(path = "feed.html")]
50
pub struct FeedTemplate<'a> {
0
51
pub title: &'a str,
52
pub profile: Option<Profile>,
53
pub statuses: Vec<StatusFromDb>,
···
7
#[derive(Template)]
8
#[template(path = "home.html")]
9
pub struct HomeTemplate<'a> {
10
+
#[allow(dead_code)]
11
pub title: &'a str,
12
pub status_options: &'a [&'a str],
13
pub profile: Option<Profile>,
···
24
#[derive(Template)]
25
#[template(path = "login.html")]
26
pub struct LoginTemplate<'a> {
27
+
#[allow(dead_code)]
28
pub title: &'a str,
29
pub error: Option<&'a str>,
30
}
···
32
#[derive(Template)]
33
#[template(path = "error.html")]
34
pub struct ErrorTemplate<'a> {
35
+
#[allow(dead_code)]
36
pub title: &'a str,
37
pub error: &'a str,
38
}
···
40
#[derive(Template)]
41
#[template(path = "status.html")]
42
pub struct StatusTemplate<'a> {
43
+
#[allow(dead_code)]
44
pub title: &'a str,
45
pub handle: String,
46
+
#[allow(dead_code)]
47
pub status_options: &'a [&'a str],
48
pub current_status: Option<StatusFromDb>,
49
pub history: Vec<StatusFromDb>,
···
53
#[derive(Template)]
54
#[template(path = "feed.html")]
55
pub struct FeedTemplate<'a> {
56
+
#[allow(dead_code)]
57
pub title: &'a str,
58
pub profile: Option<Profile>,
59
pub statuses: Vec<StatusFromDb>,
+67
-25
templates/status.html
···
105
<option value="1w">1 week</option>
106
</select>
107
</form>
108
-
109
-
{% if let Some(current) = current_status %}
110
-
<form action="/status/clear" method="post" class="clear-form">
111
-
<button type="submit" class="clear-status-btn">Clear status</button>
112
-
</form>
113
-
{% endif %}
114
</div>
115
116
<!-- Emoji Picker (hidden by default) -->
···
174
{% endif %}
175
<span class="history-time local-time" data-timestamp="{{ status.started_at.to_rfc3339() }}" data-format="short"></span>
176
</div>
0
0
0
0
0
0
0
0
177
</div>
178
{% endif %}
179
{% endfor %}
···
465
cursor: not-allowed;
466
}
467
468
-
.clear-form {
469
-
margin-top: 0.5rem;
470
-
text-align: center;
471
-
}
472
-
473
-
.clear-status-btn {
474
-
background: transparent;
475
-
border: none;
476
-
color: var(--text-tertiary);
477
-
font-size: 0.875rem;
478
-
cursor: pointer;
479
-
padding: 0.5rem;
480
-
transition: color 0.2s;
481
-
}
482
-
483
-
.clear-status-btn:hover {
484
-
color: var(--danger);
485
-
}
486
-
487
/* Emoji Picker */
488
.emoji-picker {
489
position: absolute;
···
687
align-items: center;
688
gap: 0.75rem;
689
padding: 0.5rem 0;
0
690
}
691
692
.history-emoji {
···
712
.history-time {
713
color: var(--text-tertiary);
714
font-size: 0.75rem;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
715
}
716
717
/* Nav Links */
···
1432
checkForChanges();
1433
1434
{% endif %}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1435
});
1436
</script>
1437
{%endblock content%}
···
105
<option value="1w">1 week</option>
106
</select>
107
</form>
0
0
0
0
0
0
108
</div>
109
110
<!-- Emoji Picker (hidden by default) -->
···
168
{% endif %}
169
<span class="history-time local-time" data-timestamp="{{ status.started_at.to_rfc3339() }}" data-format="short"></span>
170
</div>
171
+
{% if is_owner %}
172
+
<button type="button" class="history-delete" data-uri="{{ status.uri }}" title="Delete this status">
173
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
174
+
<line x1="18" y1="6" x2="6" y2="18"></line>
175
+
<line x1="6" y1="6" x2="18" y2="18"></line>
176
+
</svg>
177
+
</button>
178
+
{% endif %}
179
</div>
180
{% endif %}
181
{% endfor %}
···
467
cursor: not-allowed;
468
}
469
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
470
/* Emoji Picker */
471
.emoji-picker {
472
position: absolute;
···
670
align-items: center;
671
gap: 0.75rem;
672
padding: 0.5rem 0;
673
+
position: relative;
674
}
675
676
.history-emoji {
···
696
.history-time {
697
color: var(--text-tertiary);
698
font-size: 0.75rem;
699
+
}
700
+
701
+
.history-delete {
702
+
background: transparent;
703
+
border: none;
704
+
color: var(--text-tertiary);
705
+
cursor: pointer;
706
+
padding: 0.25rem;
707
+
border-radius: var(--radius-sm);
708
+
opacity: 0;
709
+
transition: all 0.2s;
710
+
}
711
+
712
+
.history-item:hover .history-delete {
713
+
opacity: 1;
714
+
}
715
+
716
+
.history-delete:hover {
717
+
background: var(--bg-tertiary);
718
+
color: var(--danger);
719
}
720
721
/* Nav Links */
···
1436
checkForChanges();
1437
1438
{% endif %}
1439
+
1440
+
// Handle delete buttons for history items
1441
+
document.querySelectorAll('.history-delete').forEach(btn => {
1442
+
btn.addEventListener('click', async (e) => {
1443
+
const uri = btn.getAttribute('data-uri');
1444
+
1445
+
if (confirm('Delete this status? This cannot be undone.')) {
1446
+
try {
1447
+
const response = await fetch('/status/delete', {
1448
+
method: 'POST',
1449
+
headers: {
1450
+
'Content-Type': 'application/json',
1451
+
},
1452
+
body: JSON.stringify({ uri })
1453
+
});
1454
+
1455
+
if (response.ok) {
1456
+
// Remove the history item from the DOM
1457
+
btn.closest('.history-item').remove();
1458
+
1459
+
// If no more history items, remove the entire history section
1460
+
const historyItems = document.querySelectorAll('.history-item');
1461
+
if (historyItems.length === 0) {
1462
+
const historySection = document.querySelector('.history');
1463
+
if (historySection) {
1464
+
historySection.remove();
1465
+
}
1466
+
}
1467
+
} else {
1468
+
alert('Failed to delete status');
1469
+
}
1470
+
} catch (error) {
1471
+
console.error('Error deleting status:', error);
1472
+
alert('Failed to delete status');
1473
+
}
1474
+
}
1475
+
});
1476
+
});
1477
});
1478
</script>
1479
{%endblock content%}