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
"futures-util",
2361
"hickory-resolver",
2362
"log",
0
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"] }
0
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;
0
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"
0
0
0
0
0
0
0
0
0
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};
0
2
3
use crate::config::Config;
4
···
56
),
57
}
58
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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 = {};
0
0
16
const categories = {
17
frequent: ['๐', '๐', 'โค๏ธ', '๐', '๐', '๐ฅ', 'โจ', '๐ฏ', '๐', '๐ช', '๐', '๐'],
18
people: [],
···
46
}
47
48
emojis[char] = keywords;
0
0
0
0
0
0
0
0
0
0
0
0
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
-
}
0
0
0
0
0
0
0
0
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
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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;
0
0
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('');
0
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';
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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];