slack status without the slack status.zzstoatzz.io/
quickslice

fix warnings

+179 -25
+106
src/main.rs
··· 791 791 expires_in: Option<String>, // e.g., "1h", "30m", "1d", etc. 792 792 } 793 793 794 + /// The post body for deleting a specific status 795 + #[derive(Serialize, Deserialize)] 796 + struct DeleteRequest { 797 + uri: String, 798 + } 799 + 794 800 /// Parse duration string like "1h", "30m", "1d" into chrono::Duration 795 801 fn parse_duration(duration_str: &str) -> Option<chrono::Duration> { 796 802 if duration_str.is_empty() { ··· 905 911 .see_other() 906 912 .respond_to(&request) 907 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 + })) 908 1013 } 909 1014 } 910 1015 } ··· 1195 1300 .service(user_status_json) 1196 1301 .service(status) 1197 1302 .service(clear_status) 1303 + .service(delete_status) 1198 1304 }) 1199 1305 .bind((host.as_str(), port))? 1200 1306 .run()
+6
src/templates.rs
··· 7 7 #[derive(Template)] 8 8 #[template(path = "home.html")] 9 9 pub struct HomeTemplate<'a> { 10 + #[allow(dead_code)] 10 11 pub title: &'a str, 11 12 pub status_options: &'a [&'a str], 12 13 pub profile: Option<Profile>, ··· 23 24 #[derive(Template)] 24 25 #[template(path = "login.html")] 25 26 pub struct LoginTemplate<'a> { 27 + #[allow(dead_code)] 26 28 pub title: &'a str, 27 29 pub error: Option<&'a str>, 28 30 } ··· 30 32 #[derive(Template)] 31 33 #[template(path = "error.html")] 32 34 pub struct ErrorTemplate<'a> { 35 + #[allow(dead_code)] 33 36 pub title: &'a str, 34 37 pub error: &'a str, 35 38 } ··· 37 40 #[derive(Template)] 38 41 #[template(path = "status.html")] 39 42 pub struct StatusTemplate<'a> { 43 + #[allow(dead_code)] 40 44 pub title: &'a str, 41 45 pub handle: String, 46 + #[allow(dead_code)] 42 47 pub status_options: &'a [&'a str], 43 48 pub current_status: Option<StatusFromDb>, 44 49 pub history: Vec<StatusFromDb>, ··· 48 53 #[derive(Template)] 49 54 #[template(path = "feed.html")] 50 55 pub struct FeedTemplate<'a> { 56 + #[allow(dead_code)] 51 57 pub title: &'a str, 52 58 pub profile: Option<Profile>, 53 59 pub statuses: Vec<StatusFromDb>,
+67 -25
templates/status.html
··· 105 105 <option value="1w">1 week</option> 106 106 </select> 107 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 108 </div> 115 109 116 110 <!-- Emoji Picker (hidden by default) --> ··· 174 168 {% endif %} 175 169 <span class="history-time local-time" data-timestamp="{{ status.started_at.to_rfc3339() }}" data-format="short"></span> 176 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 %} 177 179 </div> 178 180 {% endif %} 179 181 {% endfor %} ··· 465 467 cursor: not-allowed; 466 468 } 467 469 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 470 /* Emoji Picker */ 488 471 .emoji-picker { 489 472 position: absolute; ··· 687 670 align-items: center; 688 671 gap: 0.75rem; 689 672 padding: 0.5rem 0; 673 + position: relative; 690 674 } 691 675 692 676 .history-emoji { ··· 712 696 .history-time { 713 697 color: var(--text-tertiary); 714 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); 715 719 } 716 720 717 721 /* Nav Links */ ··· 1432 1436 checkForChanges(); 1433 1437 1434 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 + }); 1435 1477 }); 1436 1478 </script> 1437 1479 {%endblock content%}