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

style: rustfmt emoji slug loader

+127 -8
+1
Cargo.lock
··· 2360 2360 "futures-util", 2361 2361 "hickory-resolver", 2362 2362 "log", 2363 + "once_cell", 2363 2364 "rand 0.8.5", 2364 2365 "reqwest", 2365 2366 "rocketman",
+1
Cargo.toml
··· 31 31 async-trait = "0.1.88" 32 32 rand = "0.8" 33 33 reqwest = { version = "0.12", features = ["json"] } 34 + once_cell = "1.19" 34 35 35 36 [build-dependencies] 36 37 askama = "0.13"
+10
src/api/status.rs
··· 1 1 use crate::config::Config; 2 + use crate::emoji::is_builtin_slug; 2 3 use crate::resolver::HickoryDnsTxtResolver; 3 4 use crate::{ 4 5 api::auth::OAuthClientType, ··· 927 928 log::error!("Failed to create emoji dir {}: {}", app_config.emoji_dir, e); 928 929 return Ok(HttpResponse::InternalServerError().json(serde_json::json!({ 929 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(), 930 940 }))); 931 941 } 932 942
+53 -1
src/emoji.rs
··· 1 - use std::{fs, path::Path}; 1 + use once_cell::sync::OnceCell; 2 + use std::{collections::HashSet, fs, path::Path, sync::Arc}; 2 3 3 4 use crate::config::Config; 4 5 ··· 56 57 ), 57 58 } 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 12 console.log(`Loaded ${emojiData.length} emojis from CDN`); 13 13 14 14 // Transform into a simpler format for our needs 15 - const emojis = {}; 15 + const emojis = {}; // char -> keywords[] 16 + const slugs = {}; // char -> slug (first short_name fallback from name) 17 + const reserved = new Set(); // all slugs 16 18 const categories = { 17 19 frequent: ['😊', '👍', '❤️', '😂', '🎉', '🔥', '✨', '💯', '🚀', '💪', '🙏', '👏'], 18 20 people: [], ··· 46 48 } 47 49 48 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 + } 49 63 50 64 // Add to category 51 65 const categoryMap = { ··· 67 81 }); 68 82 69 83 console.log(`Built emoji database with ${Object.keys(emojis).length} emojis`); 70 - return { emojis, categories }; 84 + return { emojis, categories, slugs, reserved: Array.from(reserved) }; 71 85 } catch (error) { 72 86 console.error('Failed to load emoji data:', error); 73 87 // Fallback to a minimal set if the CDN fails ··· 89 103 objects: [], 90 104 symbols: ['❤️'], 91 105 flags: [] 92 - } 106 + }, 107 + slugs: { 108 + '😊': 'smile', 109 + '👍': 'thumbsup', 110 + '❤️': 'heart', 111 + '😂': 'joy', 112 + '🎉': 'tada' 113 + }, 114 + reserved: ['smile','thumbsup','heart','joy','tada'] 93 115 }; 94 116 } 95 117 } 96 118 97 119 // Export for use in the main page 98 - window.emojiDataLoader = { loadEmojiData }; 120 + window.emojiDataLoader = { loadEmojiData };
+15
templates/feed.html
··· 1292 1292 msg.textContent = 'choose a PNG or GIF'; 1293 1293 return; 1294 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 + } 1295 1310 const f = file.files[0]; 1296 1311 if (!['image/png','image/gif'].includes(f.type)) { 1297 1312 msg.textContent = 'only PNG or GIF';
+21 -3
templates/status.html
··· 1524 1524 if (window.emojiDataLoader) { 1525 1525 const data = await window.emojiDataLoader.loadEmojiData(); 1526 1526 emojiKeywords = data.emojis; 1527 + window.__emojiSlugs = data.slugs || {}; 1528 + window.__reservedEmojiNames = new Set(data.reserved || []); 1527 1529 1528 1530 // Load frequent emojis from API 1529 1531 let frequentEmojis = ['😊', '👍', '❤️', '😂', '🎉', '🔥', '✨', '💯', '🚀', '💪', '🙏', '👏']; // defaults ··· 1699 1701 } else { 1700 1702 // Load standard Unicode emojis 1701 1703 const emojis = emojiData[category] || []; 1702 - emojiGrid.innerHTML = emojis.map(emoji => 1703 - `<button type="button" class="emoji-option" data-emoji="${emoji}">${emoji}</button>` 1704 - ).join(''); 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(''); 1705 1708 1706 1709 // Add click handlers for standard emojis 1707 1710 emojiGrid.querySelectorAll('.emoji-option').forEach(btn => { ··· 2058 2061 msg.textContent = ''; 2059 2062 if (!file.files || file.files.length === 0) { 2060 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'; 2061 2079 return; 2062 2080 } 2063 2081 const f = file.files[0];