crawlkit_engine/
ai_bots.rs1use serde::{Deserialize, Serialize};
2
3use crate::storage::Severity;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct AiBot {
8 pub name: &'static str,
10 pub owner: &'static str,
12 pub purpose: &'static str,
14 pub severity: Severity,
16}
17
18pub struct AiBotRegistry {
23 bots: &'static [AiBot],
24}
25
26impl AiBotRegistry {
27 #[must_use]
29 pub fn default_registry() -> Self {
30 Self { bots: AI_BOTS }
31 }
32
33 #[must_use]
35 pub fn with_bots(bots: &'static [AiBot]) -> Self {
36 Self { bots }
37 }
38
39 #[must_use]
41 pub fn bots(&self) -> &'static [AiBot] {
42 self.bots
43 }
44
45 #[must_use]
47 pub fn find(&self, name: &str) -> Option<&AiBot> {
48 let lower = name.to_lowercase();
49 self.bots.iter().find(|b| b.name.to_lowercase() == lower)
50 }
51}
52
53impl AiBot {
54 #[must_use]
56 pub fn ordinal(&self) -> usize {
57 AI_BOTS
58 .iter()
59 .position(|b| std::ptr::eq(b, self))
60 .unwrap_or(0)
61 + 1
62 }
63}
64
65pub static AI_BOTS: &[AiBot] = &[
67 AiBot {
68 name: "GPTBot",
69 owner: "OpenAI",
70 purpose: "ChatGPT training and browsing",
71 severity: Severity::Error,
72 },
73 AiBot {
74 name: "Google-Extended",
75 owner: "Google",
76 purpose: "Gemini/AI Overviews training",
77 severity: Severity::Error,
78 },
79 AiBot {
80 name: "PerplexityBot",
81 owner: "Perplexity AI",
82 purpose: "Real-time search answers",
83 severity: Severity::Error,
84 },
85 AiBot {
86 name: "ClaudeBot",
87 owner: "Anthropic",
88 purpose: "Claude web search",
89 severity: Severity::Error,
90 },
91 AiBot {
92 name: "Anthropic-AI",
93 owner: "Anthropic",
94 purpose: "Claude training",
95 severity: Severity::Error,
96 },
97 AiBot {
98 name: "Bytespider",
99 owner: "ByteDance",
100 purpose: "TikTok/Douyin AI",
101 severity: Severity::Warning,
102 },
103 AiBot {
104 name: "Amazonbot",
105 owner: "Amazon",
106 purpose: "Alexa/shopping AI",
107 severity: Severity::Warning,
108 },
109 AiBot {
110 name: "Meta-ExternalAgent",
111 owner: "Meta",
112 purpose: "LLaMA training",
113 severity: Severity::Warning,
114 },
115 AiBot {
116 name: "Applebot-Extended",
117 owner: "Apple",
118 purpose: "Apple Intelligence",
119 severity: Severity::Warning,
120 },
121 AiBot {
122 name: "cohere-ai",
123 owner: "Cohere",
124 purpose: "Command R training",
125 severity: Severity::Info,
126 },
127];
128
129#[must_use]
138pub fn robots_txt_disallows_bot(robots_txt: &str, bot_name: &str) -> bool {
139 let mut matching_block: Option<bool> = None;
140 let mut disallow_all = false;
141
142 for line in robots_txt.lines() {
143 let trimmed = line.trim();
144
145 if trimmed.is_empty() || trimmed.starts_with('#') {
146 if let Some(is_matching) = matching_block {
147 if is_matching && disallow_all {
148 return true;
149 }
150 }
151 matching_block = None;
152 disallow_all = false;
153 continue;
154 }
155
156 if let Some((key, value)) = parse_directive(trimmed) {
157 match key.as_str() {
158 "user-agent" => {
159 if let Some(true) = matching_block {
160 if disallow_all {
161 return true;
162 }
163 }
164 let matches = value == "*" || value.to_lowercase() == bot_name.to_lowercase();
165 matching_block = Some(matches);
166 disallow_all = false;
167 }
168 "disallow" if matching_block.unwrap_or(false) => {
169 if value == "/" {
170 disallow_all = true;
171 } else if value.is_empty() {
172 disallow_all = false;
173 }
174 }
175 _ => {}
176 }
177 }
178 }
179
180 if let Some(true) = matching_block {
181 if disallow_all {
182 return true;
183 }
184 }
185
186 false
187}
188
189fn parse_directive(line: &str) -> Option<(String, String)> {
191 let parts: Vec<&str> = line.splitn(2, ':').collect();
192 if parts.len() == 2 {
193 Some((parts[0].trim().to_lowercase(), parts[1].trim().to_string()))
194 } else {
195 None
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_ai_bot_registry_default() {
205 let registry = AiBotRegistry::default_registry();
206 assert!(!registry.bots().is_empty());
207 assert!(registry.find("GPTBot").is_some());
208 assert!(registry.find("gptbot").is_some()); assert!(registry.find("nonexistent").is_none());
210 }
211
212 #[test]
213 fn test_robots_txt_blocks_gptbot() {
214 let robots_txt = "User-agent: GPTBot\nDisallow: /";
215 assert!(robots_txt_disallows_bot(robots_txt, "GPTBot"));
216 }
217
218 #[test]
219 fn test_robots_txt_allows_gptbot() {
220 let robots_txt = "User-agent: GPTBot\nAllow: /";
221 assert!(!robots_txt_disallows_bot(robots_txt, "GPTBot"));
222 }
223
224 #[test]
225 fn test_robots_txt_blocks_all_bots() {
226 let robots_txt = "User-agent: *\nDisallow: /";
227 assert!(robots_txt_disallows_bot(robots_txt, "GPTBot"));
228 assert!(robots_txt_disallows_bot(robots_txt, "AnyBot"));
229 }
230
231 #[test]
232 fn test_robots_txt_empty() {
233 assert!(!robots_txt_disallows_bot("", "GPTBot"));
234 }
235
236 #[test]
237 fn test_robots_txt_multiple_blocks() {
238 let robots_txt = "User-agent: *\nAllow: /\n\nUser-agent: GPTBot\nDisallow: /";
239 assert!(!robots_txt_disallows_bot(robots_txt, "Googlebot"));
240 assert!(robots_txt_disallows_bot(robots_txt, "GPTBot"));
241 }
242}