···77# Dev Mode Configuration
88DEV_MODE="false" # Enable dev mode for testing with dummy data. Access via ?dev=true query parameter when enabled.
991010-1010+# Custom Emojis
1111+# Directory to read/write custom emoji image files at runtime.
1212+# For local dev, keep under the repo:
1313+EMOJI_DIR="static/emojis"
+28-1
README.md
···2525# navigate to http://127.0.0.1:8080
2626```
27272828+### custom emojis (no redeploys)
2929+3030+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`.
3131+3232+- Local dev: add image files to `static/emojis/` (or set `EMOJI_DIR` in `.env`).
3333+- Production (Fly.io): upload files directly into the mounted volume at `/data/emojis` — no rebuild or redeploy needed.
3434+3535+Examples with Fly CLI:
3636+3737+```bash
3838+# Open an SSH console to the machine
3939+fly ssh console -a zzstoatzz-status
4040+4141+# Inside the VM, copy or fetch files into /data/emojis
4242+mkdir -p /data/emojis
4343+curl -L -o /data/emojis/my_new_emoji.png https://example.com/my_new_emoji.png
4444+```
4545+4646+Or from your machine using SFTP:
4747+4848+```bash
4949+fly ssh sftp -a zzstoatzz-status
5050+sftp> put ./static/emojis/my_new_emoji.png /data/emojis/
5151+```
5252+5353+The app serves them at `/emojis/<filename>` and lists them via `/api/custom-emojis`.
5454+2855### available commands
29563057we use [just](https://github.com/casey/just) for common tasks:
···4370- [at protocol](https://atproto.com/) (via [atrium-rs](https://github.com/sugyan/atrium))
4471- [sqlite](https://www.sqlite.org/) for local storage
4572- [jetstream](https://github.com/bluesky-social/jetstream) for firehose consumption
4646-- [fly.io](https://fly.io/) for hosting7373+- [fly.io](https://fly.io/) for hosting
···11+use crate::config::Config;
12use crate::resolver::HickoryDnsTxtResolver;
23use crate::{
34 api::auth::OAuthClientType,
···729730730731/// Get all custom emojis available on the site
731732#[get("/api/custom-emojis")]
732732-pub async fn get_custom_emojis() -> Result<impl Responder> {
733733+pub async fn get_custom_emojis(app_config: web::Data<Config>) -> Result<impl Responder> {
733734 use std::fs;
734735735736 #[derive(Serialize)]
···738739 filename: String,
739740 }
740741741741- let emojis_dir = "static/emojis";
742742+ let emojis_dir = app_config.emoji_dir.clone();
742743 let mut emojis = Vec::new();
743744744744- if let Ok(entries) = fs::read_dir(emojis_dir) {
745745+ if let Ok(entries) = fs::read_dir(&emojis_dir) {
745746 for entry in entries.flatten() {
746747 if let Some(filename) = entry.file_name().to_str() {
747748 // Only include image files
+5
src/config.rs
···31313232 /// Dev mode for testing with dummy data
3333 pub dev_mode: bool,
3434+3535+ /// Directory to serve and manage custom emojis from
3636+ pub emoji_dir: String,
3437}
35383639impl Config {
···6063 .unwrap_or_else(|_| "false".to_string())
6164 .parse()
6265 .unwrap_or(false),
6666+ // Default to static/emojis for local dev; override in prod to /data/emojis
6767+ emoji_dir: env::var("EMOJI_DIR").unwrap_or_else(|_| "static/emojis".to_string()),
6368 })
6469 }
6570}
+58
src/emoji.rs
···11+use std::{fs, path::Path};
22+33+use crate::config::Config;
44+55+/// Ensure the runtime emoji directory exists, and seed it from the bundled
66+/// `static/emojis` on first run if the runtime directory is empty.
77+pub fn init_runtime_dir(config: &Config) {
88+ let runtime_emoji_dir = &config.emoji_dir;
99+ let bundled_emoji_dir = "static/emojis";
1010+1111+ if let Err(e) = fs::create_dir_all(runtime_emoji_dir) {
1212+ log::warn!(
1313+ "Failed to ensure emoji directory exists at {}: {}",
1414+ runtime_emoji_dir,
1515+ e
1616+ );
1717+ return;
1818+ }
1919+2020+ let should_seed = runtime_emoji_dir != bundled_emoji_dir
2121+ && fs::read_dir(runtime_emoji_dir)
2222+ .map(|mut it| it.next().is_none())
2323+ .unwrap_or(false);
2424+2525+ if !should_seed {
2626+ return;
2727+ }
2828+2929+ if !Path::new(bundled_emoji_dir).exists() {
3030+ return;
3131+ }
3232+3333+ match fs::read_dir(bundled_emoji_dir) {
3434+ Ok(entries) => {
3535+ for entry in entries.flatten() {
3636+ let path = entry.path();
3737+ if let Some(name) = path.file_name() {
3838+ let dest = Path::new(runtime_emoji_dir).join(name);
3939+ if path.is_file() {
4040+ if let Err(err) = fs::copy(&path, &dest) {
4141+ log::warn!("Failed to seed emoji {:?} -> {:?}: {}", path, dest, err);
4242+ }
4343+ }
4444+ }
4545+ }
4646+ log::info!(
4747+ "Seeded emoji directory {} from {}",
4848+ runtime_emoji_dir,
4949+ bundled_emoji_dir
5050+ );
5151+ }
5252+ Err(err) => log::warn!(
5353+ "Failed to read bundled emoji directory {}: {}",
5454+ bundled_emoji_dir,
5555+ err
5656+ ),
5757+ }
5858+}
+17-2
src/main.rs
···3131mod config;
3232mod db;
3333mod dev_utils;
3434+mod emoji;
3435mod error_handler;
3536mod ingester;
3637#[allow(dead_code)]
···190191 // Create rate limiter - 30 requests per minute per IP
191192 let rate_limiter = web::Data::new(RateLimiter::new(30, Duration::from_secs(60)));
192193194194+ // Initialize runtime emoji directory (kept out of main for clarity)
195195+ emoji::init_runtime_dir(&config);
196196+193197 log::debug!("starting HTTP server at http://{host}:{port}");
194198 HttpServer::new(move || {
195199 App::new()
···210214 .build(),
211215 )
212216 .service(Files::new("/static", "static").show_files_listing())
213213- .service(Files::new("/emojis", "static/emojis").show_files_listing())
217217+ .service(
218218+ Files::new("/emojis", app_config.emoji_dir.clone())
219219+ .use_last_modified(true)
220220+ .use_etag(true)
221221+ .show_files_listing(),
222222+ )
214223 .configure(api::configure_routes)
215224 })
216225 .bind((host.as_str(), port))?
···233242 #[actix_web::test]
234243 async fn test_custom_emojis_endpoint() {
235244 // Test that the custom emojis endpoint returns JSON
236236- let app = test::init_service(App::new().service(get_custom_emojis)).await;
245245+ let cfg = crate::config::Config::from_env().expect("load config");
246246+ let app = test::init_service(
247247+ App::new()
248248+ .app_data(web::Data::new(cfg))
249249+ .service(get_custom_emojis),
250250+ )
251251+ .await;
237252238253 let req = test::TestRequest::get()
239254 .uri("/api/custom-emojis")