1pub mod emojis;
10
11use rand::prelude::IndexedRandom;
12use serde_json::{json, Value};
13use std::fs;
14use std::path::Path;
15
16pub const PLACEHOLDER: &str = "🐾";
19
20pub const MAIN_EMOJI: &str = "🦊";
22
23pub fn load_avatars() -> Value {
24 let path = Path::new(".fur/avatars.json");
25 if path.exists() {
26 let content = fs::read_to_string(path).unwrap_or_default();
27 serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
28 } else {
29 json!({})
30 }
31}
32
33pub fn save_avatars(avatars: &Value) {
34 let path = Path::new(".fur/avatars.json");
35 if let Ok(serialized) = serde_json::to_string_pretty(avatars) {
36 let _ = fs::write(path, serialized);
37 }
38}
39
40pub fn resolve_avatar(avatars: &Value, key: &str) -> (String, String) {
41 if let Some(emoji) = avatars.get(key).and_then(|v| v.as_str()) {
43 return (key.to_string(), emoji.to_string());
44 }
45
46 if let Some((name, _)) = avatars
48 .as_object()
49 .and_then(|map| map.iter().find(|(_, v)| v.as_str() == Some(key)))
50 {
51 return (name.clone(), key.to_string());
52 }
53
54 (key.to_string(), PLACEHOLDER.to_string()) }
56
57pub const AI_NAME_FRAGMENTS: &[&str] = &[
66 "gpt",
67 "claude",
68 "gemini",
69 "bard",
70 "grok",
71 "copilot",
72 "llama",
73 "mistral",
74 "deepseek",
75 "qwen",
76];
77
78pub const AI_NAME_TOKENS: &[&str] = &[
84 "ai", "ia", "bot", "llm", "agent", "assistant", "asistente", "model", "modelo",
85];
86
87pub fn is_bot_name(name: &str) -> bool {
89 let n = name.to_lowercase();
90
91 if AI_NAME_FRAGMENTS.iter().any(|m| n.contains(m)) {
92 return true;
93 }
94
95 n.split(|c: char| !c.is_alphanumeric())
96 .any(|tok| AI_NAME_TOKENS.contains(&tok))
97}
98
99pub fn get_random_emoji_for_name(name: &str) -> String {
102 if is_bot_name(name) {
103 return "🤖".to_string();
104 }
105
106 if let Some(emoji) = role_emoji(name) {
107 return emoji.to_string();
108 }
109
110 let pool = ["👤", "🧑", "👔", "🙂"];
111 pool.choose(&mut rand::rng()).unwrap_or(&"👤").to_string()
112}
113
114fn role_emoji(name: &str) -> Option<&'static str> {
116 const ROLES: &[(&str, &str)] = &[
117 ("me", MAIN_EMOJI),
118 ("self", MAIN_EMOJI),
119 ("user", MAIN_EMOJI),
120 ("boss", "💼"),
121 ("manager", "💼"),
122 ("hr", "📋"),
123 ("therapist", "🛋"),
124 ("lawyer", "⚖"),
125 ("doctor", "🩺"),
126 ("client", "🤝"),
127 ("team", "👥"),
128 ("cat", "🐱"),
129 ("dog", "🐶"),
130 ];
131
132 let key = name.trim().to_lowercase();
133
134 ROLES
135 .iter()
136 .find(|(candidate, _)| key == *candidate)
137 .map(|(_, emoji)| *emoji)
138}
139
140pub const META_KEY: &str = "meta";
153
154pub fn role_of(avatars: &Value, name: &str) -> Option<String> {
156 avatars
157 .get(META_KEY)?
158 .get(name)?
159 .get("role")?
160 .as_str()
161 .filter(|s| !s.trim().is_empty())
162 .map(|s| s.to_string())
163}
164
165pub fn kind_of(avatars: &Value, name: &str) -> String {
168 let explicit = avatars
169 .get(META_KEY)
170 .and_then(|m| m.get(name))
171 .and_then(|entry| entry.get("kind"))
172 .and_then(|v| v.as_str());
173
174 match explicit {
175 Some("ai") => "ai".to_string(),
176 Some("human") => "human".to_string(),
177 _ if is_bot_name(name) => "ai".to_string(),
178 _ => "human".to_string(),
179 }
180}
181
182pub fn set_meta(avatars: &mut Value, name: &str, field: &str, value: Option<&str>) {
186 if !avatars.is_object() {
187 *avatars = json!({});
188 }
189 if avatars.get(META_KEY).map(|m| !m.is_object()).unwrap_or(true) {
190 avatars[META_KEY] = json!({});
191 }
192
193 match value {
194 Some(v) if !v.trim().is_empty() => {
195 if avatars[META_KEY].get(name).is_none() {
196 avatars[META_KEY][name] = json!({});
197 }
198 avatars[META_KEY][name][field] = json!(v.trim());
199 }
200 _ => {
201 if let Some(entry) = avatars[META_KEY].get_mut(name) {
202 if let Some(map) = entry.as_object_mut() {
203 map.remove(field);
204 }
205 }
206 }
207 }
208
209 let empty = avatars[META_KEY]
210 .get(name)
211 .and_then(|e| e.as_object())
212 .map(|m| m.is_empty())
213 .unwrap_or(false);
214
215 if empty {
216 if let Some(meta) = avatars[META_KEY].as_object_mut() {
217 meta.remove(name);
218 }
219 }
220
221 if avatars[META_KEY]
222 .as_object()
223 .map(|m| m.is_empty())
224 .unwrap_or(false)
225 {
226 if let Some(map) = avatars.as_object_mut() {
227 map.remove(META_KEY);
228 }
229 }
230}
231
232pub fn display_name_of(avatars: &Value, name: &str) -> Option<String> {
237 avatars
238 .get(META_KEY)?
239 .get(name)?
240 .get("name")?
241 .as_str()
242 .filter(|s| !s.trim().is_empty())
243 .map(|s| s.to_string())
244}
245
246pub fn is_reserved_key(key: &str) -> bool {
248 key == "main" || key == META_KEY
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn recognises_models() {
257 assert!(is_bot_name("gpt5"));
258 assert!(is_bot_name("claude"));
259 assert!(is_bot_name("my-ai"));
260 assert!(is_bot_name("research agent"));
261 }
262
263 #[test]
264 fn recognises_spanish_and_current_models() {
265 assert!(is_bot_name("asistente"));
266 assert!(is_bot_name("mi-ia"));
267 assert!(is_bot_name("llama3"));
268 assert!(is_bot_name("deepseek-r1"));
269 assert!(!is_bot_name("maria"));
270 assert!(!is_bot_name("diana"));
271 }
272
273 #[test]
274 fn does_not_mistake_people_for_bots() {
275 assert!(!is_bot_name("claire"));
277 assert!(!is_bot_name("raina"));
278 assert!(!is_bot_name("robotics_lab"));
279 assert!(!is_bot_name("management"));
280 }
281
282 #[test]
283 fn roles_get_stable_emoji() {
284 assert_eq!(get_random_emoji_for_name("hr"), "📋");
285 assert_eq!(get_random_emoji_for_name("me"), MAIN_EMOJI);
286 }
287}