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

style: rustfmt emoji slug loader

+127 -8
+1
Cargo.lock
··· 2360 "futures-util", 2361 "hickory-resolver", 2362 "log", 2363 "rand 0.8.5", 2364 "reqwest", 2365 "rocketman",
··· 2360 "futures-util", 2361 "hickory-resolver", 2362 "log", 2363 + "once_cell", 2364 "rand 0.8.5", 2365 "reqwest", 2366 "rocketman",
+1
Cargo.toml
··· 31 async-trait = "0.1.88" 32 rand = "0.8" 33 reqwest = { version = "0.12", features = ["json"] } 34 35 [build-dependencies] 36 askama = "0.13"
··· 31 async-trait = "0.1.88" 32 rand = "0.8" 33 reqwest = { version = "0.12", features = ["json"] } 34 + once_cell = "1.19" 35 36 [build-dependencies] 37 askama = "0.13"
+10
src/api/status.rs
··· 1 use crate::config::Config; 2 use crate::resolver::HickoryDnsTxtResolver; 3 use crate::{ 4 api::auth::OAuthClientType, ··· 927 log::error!("Failed to create emoji dir {}: {}", app_config.emoji_dir, e); 928 return Ok(HttpResponse::InternalServerError().json(serde_json::json!({ 929 "error": "Filesystem error" 930 }))); 931 } 932
··· 1 use crate::config::Config; 2 + use crate::emoji::is_builtin_slug; 3 use crate::resolver::HickoryDnsTxtResolver; 4 use crate::{ 5 api::auth::OAuthClientType, ··· 928 log::error!("Failed to create emoji dir {}: {}", app_config.emoji_dir, e); 929 return Ok(HttpResponse::InternalServerError().json(serde_json::json!({ 930 "error": "Filesystem error" 931 + }))); 932 + } 933 + 934 + // If user provided a name explicitly and it conflicts with a builtin emoji slug, reject 935 + if desired_name.is_some() && is_builtin_slug(&safe.to_lowercase()).await { 936 + return Ok(HttpResponse::Conflict().json(serde_json::json!({ 937 + "error": "Name is reserved by a standard emoji.", 938 + "code": "name_exists", 939 + "name": safe.to_lowercase(), 940 }))); 941 } 942
+53 -1
src/emoji.rs
··· 1 - use std::{fs, path::Path}; 2 3 use crate::config::Config; 4 ··· 56 ), 57 } 58 }
··· 1 + use once_cell::sync::OnceCell; 2 + use std::{collections::HashSet, fs, path::Path, sync::Arc}; 3 4 use crate::config::Config; 5 ··· 57 ), 58 } 59 } 60 + 61 + static BUILTIN_SLUGS: OnceCell<Arc<HashSet<String>>> = OnceCell::new(); 62 + 63 + async fn load_builtin_slugs_inner() -> Arc<HashSet<String>> { 64 + // Fetch emoji data and collect first short_name as slug 65 + let url = "https://cdn.jsdelivr.net/npm/emoji-datasource@15.1.0/emoji.json"; 66 + let client = reqwest::Client::new(); 67 + let mut set = HashSet::new(); 68 + if let Ok(resp) = client.get(url).send().await { 69 + if let Ok(json) = resp.json::<serde_json::Value>().await { 70 + if let Some(arr) = json.as_array() { 71 + for item in arr { 72 + if let Some(shorts) = item.get("short_names").and_then(|v| v.as_array()) { 73 + if let Some(first) = shorts.first().and_then(|v| v.as_str()) { 74 + set.insert(first.to_lowercase()); 75 + } 76 + } else if let Some(name) = item.get("name").and_then(|v| v.as_str()) { 77 + // Fallback: slugify the name 78 + let slug: String = name 79 + .chars() 80 + .map(|c| { 81 + if c.is_ascii_alphanumeric() { 82 + c.to_ascii_lowercase() 83 + } else { 84 + '-' 85 + } 86 + }) 87 + .collect::<String>() 88 + .trim_matches('-') 89 + .to_string(); 90 + if !slug.is_empty() { 91 + set.insert(slug); 92 + } 93 + } 94 + } 95 + } 96 + } 97 + } 98 + Arc::new(set) 99 + } 100 + 101 + pub async fn is_builtin_slug(name: &str) -> bool { 102 + let name = name.to_lowercase(); 103 + if let Some(cache) = BUILTIN_SLUGS.get() { 104 + return cache.contains(&name); 105 + } 106 + let set = load_builtin_slugs_inner().await; 107 + let contains = set.contains(&name); 108 + let _ = BUILTIN_SLUGS.set(set); 109 + contains 110 + }
+26 -4
static/emoji-data.js
··· 12 console.log(`Loaded ${emojiData.length} emojis from CDN`); 13 14 // Transform into a simpler format for our needs 15 - const emojis = {}; 16 const categories = { 17 frequent: ['๐Ÿ˜Š', '๐Ÿ‘', 'โค๏ธ', '๐Ÿ˜‚', '๐ŸŽ‰', '๐Ÿ”ฅ', 'โœจ', '๐Ÿ’ฏ', '๐Ÿš€', '๐Ÿ’ช', '๐Ÿ™', '๐Ÿ‘'], 18 people: [], ··· 46 } 47 48 emojis[char] = keywords; 49 50 // Add to category 51 const categoryMap = { ··· 67 }); 68 69 console.log(`Built emoji database with ${Object.keys(emojis).length} emojis`); 70 - return { emojis, categories }; 71 } catch (error) { 72 console.error('Failed to load emoji data:', error); 73 // Fallback to a minimal set if the CDN fails ··· 89 objects: [], 90 symbols: ['โค๏ธ'], 91 flags: [] 92 - } 93 }; 94 } 95 } 96 97 // Export for use in the main page 98 - window.emojiDataLoader = { loadEmojiData };
··· 12 console.log(`Loaded ${emojiData.length} emojis from CDN`); 13 14 // Transform into a simpler format for our needs 15 + const emojis = {}; // char -> keywords[] 16 + const slugs = {}; // char -> slug (first short_name fallback from name) 17 + const reserved = new Set(); // all slugs 18 const categories = { 19 frequent: ['๐Ÿ˜Š', '๐Ÿ‘', 'โค๏ธ', '๐Ÿ˜‚', '๐ŸŽ‰', '๐Ÿ”ฅ', 'โœจ', '๐Ÿ’ฏ', '๐Ÿš€', '๐Ÿ’ช', '๐Ÿ™', '๐Ÿ‘'], 20 people: [], ··· 48 } 49 50 emojis[char] = keywords; 51 + 52 + // Pick a slug: prefer the first short_name 53 + let slug = null; 54 + if (emoji.short_names && emoji.short_names.length > 0) { 55 + slug = emoji.short_names[0].toLowerCase(); 56 + } else if (emoji.name) { 57 + slug = emoji.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); 58 + } 59 + if (slug) { 60 + slugs[char] = slug; 61 + reserved.add(slug); 62 + } 63 64 // Add to category 65 const categoryMap = { ··· 81 }); 82 83 console.log(`Built emoji database with ${Object.keys(emojis).length} emojis`); 84 + return { emojis, categories, slugs, reserved: Array.from(reserved) }; 85 } catch (error) { 86 console.error('Failed to load emoji data:', error); 87 // Fallback to a minimal set if the CDN fails ··· 103 objects: [], 104 symbols: ['โค๏ธ'], 105 flags: [] 106 + }, 107 + slugs: { 108 + '๐Ÿ˜Š': 'smile', 109 + '๐Ÿ‘': 'thumbsup', 110 + 'โค๏ธ': 'heart', 111 + '๐Ÿ˜‚': 'joy', 112 + '๐ŸŽ‰': 'tada' 113 + }, 114 + reserved: ['smile','thumbsup','heart','joy','tada'] 115 }; 116 } 117 } 118 119 // Export for use in the main page 120 + window.emojiDataLoader = { loadEmojiData };
+15
templates/feed.html
··· 1292 msg.textContent = 'choose a PNG or GIF'; 1293 return; 1294 } 1295 const f = file.files[0]; 1296 if (!['image/png','image/gif'].includes(f.type)) { 1297 msg.textContent = 'only PNG or GIF';
··· 1292 msg.textContent = 'choose a PNG or GIF'; 1293 return; 1294 } 1295 + // Require a name; prefill from filename if empty 1296 + if (!name.value.trim().length) { 1297 + const base = (file.files[0].name || '').replace(/\.[^.]+$/, ''); 1298 + const sanitized = base.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, ''); 1299 + name.value = sanitized || ''; 1300 + } 1301 + if (!name.value.trim().length) { 1302 + msg.textContent = 'please choose a name'; 1303 + return; 1304 + } 1305 + // Client-side reserved check (best-effort) 1306 + if (window.__reservedEmojiNames && window.__reservedEmojiNames.has(name.value.trim().toLowerCase())) { 1307 + msg.textContent = 'that name is reserved by a standard emoji'; 1308 + return; 1309 + } 1310 const f = file.files[0]; 1311 if (!['image/png','image/gif'].includes(f.type)) { 1312 msg.textContent = 'only PNG or GIF';
+21 -3
templates/status.html
··· 1524 if (window.emojiDataLoader) { 1525 const data = await window.emojiDataLoader.loadEmojiData(); 1526 emojiKeywords = data.emojis; 1527 1528 // Load frequent emojis from API 1529 let frequentEmojis = ['๐Ÿ˜Š', '๐Ÿ‘', 'โค๏ธ', '๐Ÿ˜‚', '๐ŸŽ‰', '๐Ÿ”ฅ', 'โœจ', '๐Ÿ’ฏ', '๐Ÿš€', '๐Ÿ’ช', '๐Ÿ™', '๐Ÿ‘']; // defaults ··· 1699 } else { 1700 // Load standard Unicode emojis 1701 const emojis = emojiData[category] || []; 1702 - emojiGrid.innerHTML = emojis.map(emoji => 1703 - `<button type="button" class="emoji-option" data-emoji="${emoji}">${emoji}</button>` 1704 - ).join(''); 1705 1706 // Add click handlers for standard emojis 1707 emojiGrid.querySelectorAll('.emoji-option').forEach(btn => { ··· 2058 msg.textContent = ''; 2059 if (!file.files || file.files.length === 0) { 2060 msg.textContent = 'choose a PNG or GIF'; 2061 return; 2062 } 2063 const f = file.files[0];
··· 1524 if (window.emojiDataLoader) { 1525 const data = await window.emojiDataLoader.loadEmojiData(); 1526 emojiKeywords = data.emojis; 1527 + window.__emojiSlugs = data.slugs || {}; 1528 + window.__reservedEmojiNames = new Set(data.reserved || []); 1529 1530 // Load frequent emojis from API 1531 let frequentEmojis = ['๐Ÿ˜Š', '๐Ÿ‘', 'โค๏ธ', '๐Ÿ˜‚', '๐ŸŽ‰', '๐Ÿ”ฅ', 'โœจ', '๐Ÿ’ฏ', '๐Ÿš€', '๐Ÿ’ช', '๐Ÿ™', '๐Ÿ‘']; // defaults ··· 1701 } else { 1702 // Load standard Unicode emojis 1703 const emojis = emojiData[category] || []; 1704 + emojiGrid.innerHTML = emojis.map(emoji => { 1705 + const slug = (window.__emojiSlugs && window.__emojiSlugs[emoji]) || ''; 1706 + return `<button type="button" class="emoji-option" data-emoji="${emoji}" data-name="${slug}" title="${slug}">${emoji}</button>`; 1707 + }).join(''); 1708 1709 // Add click handlers for standard emojis 1710 emojiGrid.querySelectorAll('.emoji-option').forEach(btn => { ··· 2061 msg.textContent = ''; 2062 if (!file.files || file.files.length === 0) { 2063 msg.textContent = 'choose a PNG or GIF'; 2064 + return; 2065 + } 2066 + // Require a name; prefill from filename if empty 2067 + if (!name.value.trim().length) { 2068 + const base = (file.files[0].name || '').replace(/\.[^.]+$/, ''); 2069 + const sanitized = base.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, ''); 2070 + name.value = sanitized || ''; 2071 + } 2072 + if (!name.value.trim().length) { 2073 + msg.textContent = 'please choose a name'; 2074 + return; 2075 + } 2076 + // Client-side reserved check (best-effort) 2077 + if (window.__reservedEmojiNames && window.__reservedEmojiNames.has(name.value.trim().toLowerCase())) { 2078 + msg.textContent = 'that name is reserved by a standard emoji'; 2079 return; 2080 } 2081 const f = file.files[0];