+4
-1
.env.template
+4
-1
.env.template
···
7
7
# Dev Mode Configuration
8
8
DEV_MODE="false" # Enable dev mode for testing with dummy data. Access via ?dev=true query parameter when enabled.
9
9
10
-
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
+28
-1
README.md
···
25
25
# navigate to http://127.0.0.1:8080
26
26
```
27
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
+
28
55
### available commands
29
56
30
57
we use [just](https://github.com/casey/just) for common tasks:
···
43
70
- [at protocol](https://atproto.com/) (via [atrium-rs](https://github.com/sugyan/atrium))
44
71
- [sqlite](https://www.sqlite.org/) for local storage
45
72
- [jetstream](https://github.com/bluesky-social/jetstream) for firehose consumption
46
-
- [fly.io](https://fly.io/) for hosting
73
+
- [fly.io](https://fly.io/) for hosting
+2
-1
fly.toml
+2
-1
fly.toml
···
10
10
DATABASE_URL = "sqlite:///data/status.db"
11
11
ENABLE_FIREHOSE = "true"
12
12
OAUTH_REDIRECT_BASE = "https://status.zzstoatzz.io"
13
+
EMOJI_DIR = "/data/emojis"
13
14
14
15
[http_service]
15
16
internal_port = 8080
···
30
31
[[vm]]
31
32
cpu_kind = "shared"
32
33
cpus = 1
33
-
memory_mb = 512
34
+
memory_mb = 512
+4
-3
src/api/status.rs
+4
-3
src/api/status.rs
···
1
+
use crate::config::Config;
1
2
use crate::resolver::HickoryDnsTxtResolver;
2
3
use crate::{
3
4
api::auth::OAuthClientType,
···
729
730
730
731
/// Get all custom emojis available on the site
731
732
#[get("/api/custom-emojis")]
732
-
pub async fn get_custom_emojis() -> Result<impl Responder> {
733
+
pub async fn get_custom_emojis(app_config: web::Data<Config>) -> Result<impl Responder> {
733
734
use std::fs;
734
735
735
736
#[derive(Serialize)]
···
738
739
filename: String,
739
740
}
740
741
741
-
let emojis_dir = "static/emojis";
742
+
let emojis_dir = app_config.emoji_dir.clone();
742
743
let mut emojis = Vec::new();
743
744
744
-
if let Ok(entries) = fs::read_dir(emojis_dir) {
745
+
if let Ok(entries) = fs::read_dir(&emojis_dir) {
745
746
for entry in entries.flatten() {
746
747
if let Some(filename) = entry.file_name().to_str() {
747
748
// Only include image files
+5
src/config.rs
+5
src/config.rs
···
31
31
32
32
/// Dev mode for testing with dummy data
33
33
pub dev_mode: bool,
34
+
35
+
/// Directory to serve and manage custom emojis from
36
+
pub emoji_dir: String,
34
37
}
35
38
36
39
impl Config {
···
60
63
.unwrap_or_else(|_| "false".to_string())
61
64
.parse()
62
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()),
63
68
})
64
69
}
65
70
}
+58
src/emoji.rs
+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
+17
-2
src/main.rs
···
31
31
mod config;
32
32
mod db;
33
33
mod dev_utils;
34
+
mod emoji;
34
35
mod error_handler;
35
36
mod ingester;
36
37
#[allow(dead_code)]
···
190
191
// Create rate limiter - 30 requests per minute per IP
191
192
let rate_limiter = web::Data::new(RateLimiter::new(30, Duration::from_secs(60)));
192
193
194
+
// Initialize runtime emoji directory (kept out of main for clarity)
195
+
emoji::init_runtime_dir(&config);
196
+
193
197
log::debug!("starting HTTP server at http://{host}:{port}");
194
198
HttpServer::new(move || {
195
199
App::new()
···
210
214
.build(),
211
215
)
212
216
.service(Files::new("/static", "static").show_files_listing())
213
-
.service(Files::new("/emojis", "static/emojis").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
+
)
214
223
.configure(api::configure_routes)
215
224
})
216
225
.bind((host.as_str(), port))?
···
233
242
#[actix_web::test]
234
243
async fn test_custom_emojis_endpoint() {
235
244
// Test that the custom emojis endpoint returns JSON
236
-
let app = test::init_service(App::new().service(get_custom_emojis)).await;
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;
237
252
238
253
let req = test::TestRequest::get()
239
254
.uri("/api/custom-emojis")