tangled
alpha
login
or
join now
zzstoatzz.io
/
status
0
fork
atom
slack status without the slack
status.zzstoatzz.io/
quickslice
0
fork
atom
overview
issues
pulls
pipelines
style: rustfmt emoji slug loader
zzstoatzz.io
6 months ago
262adcfa
a6b10650
+127
-8
7 changed files
expand all
collapse all
unified
split
Cargo.lock
Cargo.toml
src
api
status.rs
emoji.rs
static
emoji-data.js
templates
feed.html
status.html
+1
Cargo.lock
···
2360
2360
"futures-util",
2361
2361
"hickory-resolver",
2362
2362
"log",
2363
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
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
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
931
+
})));
932
932
+
}
933
933
+
934
934
+
// If user provided a name explicitly and it conflicts with a builtin emoji slug, reject
935
935
+
if desired_name.is_some() && is_builtin_slug(&safe.to_lowercase()).await {
936
936
+
return Ok(HttpResponse::Conflict().json(serde_json::json!({
937
937
+
"error": "Name is reserved by a standard emoji.",
938
938
+
"code": "name_exists",
939
939
+
"name": safe.to_lowercase(),
930
940
})));
931
941
}
932
942
+53
-1
src/emoji.rs
···
1
1
-
use std::{fs, path::Path};
1
1
+
use once_cell::sync::OnceCell;
2
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
60
+
61
61
+
static BUILTIN_SLUGS: OnceCell<Arc<HashSet<String>>> = OnceCell::new();
62
62
+
63
63
+
async fn load_builtin_slugs_inner() -> Arc<HashSet<String>> {
64
64
+
// Fetch emoji data and collect first short_name as slug
65
65
+
let url = "https://cdn.jsdelivr.net/npm/emoji-datasource@15.1.0/emoji.json";
66
66
+
let client = reqwest::Client::new();
67
67
+
let mut set = HashSet::new();
68
68
+
if let Ok(resp) = client.get(url).send().await {
69
69
+
if let Ok(json) = resp.json::<serde_json::Value>().await {
70
70
+
if let Some(arr) = json.as_array() {
71
71
+
for item in arr {
72
72
+
if let Some(shorts) = item.get("short_names").and_then(|v| v.as_array()) {
73
73
+
if let Some(first) = shorts.first().and_then(|v| v.as_str()) {
74
74
+
set.insert(first.to_lowercase());
75
75
+
}
76
76
+
} else if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
77
77
+
// Fallback: slugify the name
78
78
+
let slug: String = name
79
79
+
.chars()
80
80
+
.map(|c| {
81
81
+
if c.is_ascii_alphanumeric() {
82
82
+
c.to_ascii_lowercase()
83
83
+
} else {
84
84
+
'-'
85
85
+
}
86
86
+
})
87
87
+
.collect::<String>()
88
88
+
.trim_matches('-')
89
89
+
.to_string();
90
90
+
if !slug.is_empty() {
91
91
+
set.insert(slug);
92
92
+
}
93
93
+
}
94
94
+
}
95
95
+
}
96
96
+
}
97
97
+
}
98
98
+
Arc::new(set)
99
99
+
}
100
100
+
101
101
+
pub async fn is_builtin_slug(name: &str) -> bool {
102
102
+
let name = name.to_lowercase();
103
103
+
if let Some(cache) = BUILTIN_SLUGS.get() {
104
104
+
return cache.contains(&name);
105
105
+
}
106
106
+
let set = load_builtin_slugs_inner().await;
107
107
+
let contains = set.contains(&name);
108
108
+
let _ = BUILTIN_SLUGS.set(set);
109
109
+
contains
110
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
15
-
const emojis = {};
15
15
+
const emojis = {}; // char -> keywords[]
16
16
+
const slugs = {}; // char -> slug (first short_name fallback from name)
17
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
51
+
52
52
+
// Pick a slug: prefer the first short_name
53
53
+
let slug = null;
54
54
+
if (emoji.short_names && emoji.short_names.length > 0) {
55
55
+
slug = emoji.short_names[0].toLowerCase();
56
56
+
} else if (emoji.name) {
57
57
+
slug = emoji.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
58
58
+
}
59
59
+
if (slug) {
60
60
+
slugs[char] = slug;
61
61
+
reserved.add(slug);
62
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
70
-
return { emojis, categories };
84
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
92
-
}
106
106
+
},
107
107
+
slugs: {
108
108
+
'😊': 'smile',
109
109
+
'👍': 'thumbsup',
110
110
+
'❤️': 'heart',
111
111
+
'😂': 'joy',
112
112
+
'🎉': 'tada'
113
113
+
},
114
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
98
-
window.emojiDataLoader = { loadEmojiData };
120
120
+
window.emojiDataLoader = { loadEmojiData };
+15
templates/feed.html
···
1292
1292
msg.textContent = 'choose a PNG or GIF';
1293
1293
return;
1294
1294
}
1295
1295
+
// Require a name; prefill from filename if empty
1296
1296
+
if (!name.value.trim().length) {
1297
1297
+
const base = (file.files[0].name || '').replace(/\.[^.]+$/, '');
1298
1298
+
const sanitized = base.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
1299
1299
+
name.value = sanitized || '';
1300
1300
+
}
1301
1301
+
if (!name.value.trim().length) {
1302
1302
+
msg.textContent = 'please choose a name';
1303
1303
+
return;
1304
1304
+
}
1305
1305
+
// Client-side reserved check (best-effort)
1306
1306
+
if (window.__reservedEmojiNames && window.__reservedEmojiNames.has(name.value.trim().toLowerCase())) {
1307
1307
+
msg.textContent = 'that name is reserved by a standard emoji';
1308
1308
+
return;
1309
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
1527
+
window.__emojiSlugs = data.slugs || {};
1528
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
1702
-
emojiGrid.innerHTML = emojis.map(emoji =>
1703
1703
-
`<button type="button" class="emoji-option" data-emoji="${emoji}">${emoji}</button>`
1704
1704
-
).join('');
1704
1704
+
emojiGrid.innerHTML = emojis.map(emoji => {
1705
1705
+
const slug = (window.__emojiSlugs && window.__emojiSlugs[emoji]) || '';
1706
1706
+
return `<button type="button" class="emoji-option" data-emoji="${emoji}" data-name="${slug}" title="${slug}">${emoji}</button>`;
1707
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
2064
+
return;
2065
2065
+
}
2066
2066
+
// Require a name; prefill from filename if empty
2067
2067
+
if (!name.value.trim().length) {
2068
2068
+
const base = (file.files[0].name || '').replace(/\.[^.]+$/, '');
2069
2069
+
const sanitized = base.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
2070
2070
+
name.value = sanitized || '';
2071
2071
+
}
2072
2072
+
if (!name.value.trim().length) {
2073
2073
+
msg.textContent = 'please choose a name';
2074
2074
+
return;
2075
2075
+
}
2076
2076
+
// Client-side reserved check (best-effort)
2077
2077
+
if (window.__reservedEmojiNames && window.__reservedEmojiNames.has(name.value.trim().toLowerCase())) {
2078
2078
+
msg.textContent = 'that name is reserved by a standard emoji';
2061
2079
return;
2062
2080
}
2063
2081
const f = file.files[0];