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

style: rustfmt after emoji runtime refactor

+118 -8
+4 -1
.env.template
··· 7 # Dev Mode Configuration 8 DEV_MODE="false" # Enable dev mode for testing with dummy data. Access via ?dev=true query parameter when enabled. 9 10 -
··· 7 # Dev Mode Configuration 8 DEV_MODE="false" # Enable dev mode for testing with dummy data. Access via ?dev=true query parameter when enabled. 9 10 + # Custom Emojis 11 + # Directory to read/write custom emoji image files at runtime. 12 + # For local dev, keep under the repo: 13 + EMOJI_DIR="static/emojis"
+28 -1
README.md
··· 25 # navigate to http://127.0.0.1:8080 26 ``` 27 28 ### available commands 29 30 we use [just](https://github.com/casey/just) for common tasks: ··· 43 - [at protocol](https://atproto.com/) (via [atrium-rs](https://github.com/sugyan/atrium)) 44 - [sqlite](https://www.sqlite.org/) for local storage 45 - [jetstream](https://github.com/bluesky-social/jetstream) for firehose consumption 46 - - [fly.io](https://fly.io/) for hosting
··· 25 # navigate to http://127.0.0.1:8080 26 ``` 27 28 + ### custom emojis (no redeploys) 29 + 30 + Emojis are now served from a runtime directory configured by `EMOJI_DIR` (defaults to `static/emojis` locally; set to `/data/emojis` on Fly.io). On startup, if the runtime emoji directory is empty, it will be seeded from the bundled `static/emojis`. 31 + 32 + - Local dev: add image files to `static/emojis/` (or set `EMOJI_DIR` in `.env`). 33 + - Production (Fly.io): upload files directly into the mounted volume at `/data/emojis` — no rebuild or redeploy needed. 34 + 35 + Examples with Fly CLI: 36 + 37 + ```bash 38 + # Open an SSH console to the machine 39 + fly ssh console -a zzstoatzz-status 40 + 41 + # Inside the VM, copy or fetch files into /data/emojis 42 + mkdir -p /data/emojis 43 + curl -L -o /data/emojis/my_new_emoji.png https://example.com/my_new_emoji.png 44 + ``` 45 + 46 + Or from your machine using SFTP: 47 + 48 + ```bash 49 + fly ssh sftp -a zzstoatzz-status 50 + sftp> put ./static/emojis/my_new_emoji.png /data/emojis/ 51 + ``` 52 + 53 + The app serves them at `/emojis/<filename>` and lists them via `/api/custom-emojis`. 54 + 55 ### available commands 56 57 we use [just](https://github.com/casey/just) for common tasks: ··· 70 - [at protocol](https://atproto.com/) (via [atrium-rs](https://github.com/sugyan/atrium)) 71 - [sqlite](https://www.sqlite.org/) for local storage 72 - [jetstream](https://github.com/bluesky-social/jetstream) for firehose consumption 73 + - [fly.io](https://fly.io/) for hosting
+2 -1
fly.toml
··· 10 DATABASE_URL = "sqlite:///data/status.db" 11 ENABLE_FIREHOSE = "true" 12 OAUTH_REDIRECT_BASE = "https://status.zzstoatzz.io" 13 14 [http_service] 15 internal_port = 8080 ··· 30 [[vm]] 31 cpu_kind = "shared" 32 cpus = 1 33 - memory_mb = 512
··· 10 DATABASE_URL = "sqlite:///data/status.db" 11 ENABLE_FIREHOSE = "true" 12 OAUTH_REDIRECT_BASE = "https://status.zzstoatzz.io" 13 + EMOJI_DIR = "/data/emojis" 14 15 [http_service] 16 internal_port = 8080 ··· 31 [[vm]] 32 cpu_kind = "shared" 33 cpus = 1 34 + memory_mb = 512
+4 -3
src/api/status.rs
··· 1 use crate::resolver::HickoryDnsTxtResolver; 2 use crate::{ 3 api::auth::OAuthClientType, ··· 729 730 /// Get all custom emojis available on the site 731 #[get("/api/custom-emojis")] 732 - pub async fn get_custom_emojis() -> Result<impl Responder> { 733 use std::fs; 734 735 #[derive(Serialize)] ··· 738 filename: String, 739 } 740 741 - let emojis_dir = "static/emojis"; 742 let mut emojis = Vec::new(); 743 744 - if let Ok(entries) = fs::read_dir(emojis_dir) { 745 for entry in entries.flatten() { 746 if let Some(filename) = entry.file_name().to_str() { 747 // Only include image files
··· 1 + use crate::config::Config; 2 use crate::resolver::HickoryDnsTxtResolver; 3 use crate::{ 4 api::auth::OAuthClientType, ··· 730 731 /// Get all custom emojis available on the site 732 #[get("/api/custom-emojis")] 733 + pub async fn get_custom_emojis(app_config: web::Data<Config>) -> Result<impl Responder> { 734 use std::fs; 735 736 #[derive(Serialize)] ··· 739 filename: String, 740 } 741 742 + let emojis_dir = app_config.emoji_dir.clone(); 743 let mut emojis = Vec::new(); 744 745 + if let Ok(entries) = fs::read_dir(&emojis_dir) { 746 for entry in entries.flatten() { 747 if let Some(filename) = entry.file_name().to_str() { 748 // Only include image files
+5
src/config.rs
··· 31 32 /// Dev mode for testing with dummy data 33 pub dev_mode: bool, 34 } 35 36 impl Config { ··· 60 .unwrap_or_else(|_| "false".to_string()) 61 .parse() 62 .unwrap_or(false), 63 }) 64 } 65 }
··· 31 32 /// Dev mode for testing with dummy data 33 pub dev_mode: bool, 34 + 35 + /// Directory to serve and manage custom emojis from 36 + pub emoji_dir: String, 37 } 38 39 impl Config { ··· 63 .unwrap_or_else(|_| "false".to_string()) 64 .parse() 65 .unwrap_or(false), 66 + // Default to static/emojis for local dev; override in prod to /data/emojis 67 + emoji_dir: env::var("EMOJI_DIR").unwrap_or_else(|_| "static/emojis".to_string()), 68 }) 69 } 70 }
+58
src/emoji.rs
···
··· 1 + use std::{fs, path::Path}; 2 + 3 + use crate::config::Config; 4 + 5 + /// Ensure the runtime emoji directory exists, and seed it from the bundled 6 + /// `static/emojis` on first run if the runtime directory is empty. 7 + pub fn init_runtime_dir(config: &Config) { 8 + let runtime_emoji_dir = &config.emoji_dir; 9 + let bundled_emoji_dir = "static/emojis"; 10 + 11 + if let Err(e) = fs::create_dir_all(runtime_emoji_dir) { 12 + log::warn!( 13 + "Failed to ensure emoji directory exists at {}: {}", 14 + runtime_emoji_dir, 15 + e 16 + ); 17 + return; 18 + } 19 + 20 + let should_seed = runtime_emoji_dir != bundled_emoji_dir 21 + && fs::read_dir(runtime_emoji_dir) 22 + .map(|mut it| it.next().is_none()) 23 + .unwrap_or(false); 24 + 25 + if !should_seed { 26 + return; 27 + } 28 + 29 + if !Path::new(bundled_emoji_dir).exists() { 30 + return; 31 + } 32 + 33 + match fs::read_dir(bundled_emoji_dir) { 34 + Ok(entries) => { 35 + for entry in entries.flatten() { 36 + let path = entry.path(); 37 + if let Some(name) = path.file_name() { 38 + let dest = Path::new(runtime_emoji_dir).join(name); 39 + if path.is_file() { 40 + if let Err(err) = fs::copy(&path, &dest) { 41 + log::warn!("Failed to seed emoji {:?} -> {:?}: {}", path, dest, err); 42 + } 43 + } 44 + } 45 + } 46 + log::info!( 47 + "Seeded emoji directory {} from {}", 48 + runtime_emoji_dir, 49 + bundled_emoji_dir 50 + ); 51 + } 52 + Err(err) => log::warn!( 53 + "Failed to read bundled emoji directory {}: {}", 54 + bundled_emoji_dir, 55 + err 56 + ), 57 + } 58 + }
+17 -2
src/main.rs
··· 31 mod config; 32 mod db; 33 mod dev_utils; 34 mod error_handler; 35 mod ingester; 36 #[allow(dead_code)] ··· 190 // Create rate limiter - 30 requests per minute per IP 191 let rate_limiter = web::Data::new(RateLimiter::new(30, Duration::from_secs(60))); 192 193 log::debug!("starting HTTP server at http://{host}:{port}"); 194 HttpServer::new(move || { 195 App::new() ··· 210 .build(), 211 ) 212 .service(Files::new("/static", "static").show_files_listing()) 213 - .service(Files::new("/emojis", "static/emojis").show_files_listing()) 214 .configure(api::configure_routes) 215 }) 216 .bind((host.as_str(), port))? ··· 233 #[actix_web::test] 234 async fn test_custom_emojis_endpoint() { 235 // Test that the custom emojis endpoint returns JSON 236 - let app = test::init_service(App::new().service(get_custom_emojis)).await; 237 238 let req = test::TestRequest::get() 239 .uri("/api/custom-emojis")
··· 31 mod config; 32 mod db; 33 mod dev_utils; 34 + mod emoji; 35 mod error_handler; 36 mod ingester; 37 #[allow(dead_code)] ··· 191 // Create rate limiter - 30 requests per minute per IP 192 let rate_limiter = web::Data::new(RateLimiter::new(30, Duration::from_secs(60))); 193 194 + // Initialize runtime emoji directory (kept out of main for clarity) 195 + emoji::init_runtime_dir(&config); 196 + 197 log::debug!("starting HTTP server at http://{host}:{port}"); 198 HttpServer::new(move || { 199 App::new() ··· 214 .build(), 215 ) 216 .service(Files::new("/static", "static").show_files_listing()) 217 + .service( 218 + Files::new("/emojis", app_config.emoji_dir.clone()) 219 + .use_last_modified(true) 220 + .use_etag(true) 221 + .show_files_listing(), 222 + ) 223 .configure(api::configure_routes) 224 }) 225 .bind((host.as_str(), port))? ··· 242 #[actix_web::test] 243 async fn test_custom_emojis_endpoint() { 244 // Test that the custom emojis endpoint returns JSON 245 + let cfg = crate::config::Config::from_env().expect("load config"); 246 + let app = test::init_service( 247 + App::new() 248 + .app_data(web::Data::new(cfg)) 249 + .service(get_custom_emojis), 250 + ) 251 + .await; 252 253 let req = test::TestRequest::get() 254 .uri("/api/custom-emojis")