Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-public-site/src/limit.rs

//! **The crawlers robots.txt speaks to by name**, and which kind a `User-Agent` is.
//!
//! The rate limit itself is not here any more (GUN-GUEST, owner 2026-10-30): EVERY
//! request to the console, a crawler's, a guest's or a signed-in person's, on every route,
//! passes the console's ONE limiter (`gunnar-ui-server`'s `throttle`), keyed per client.

/// The kind of client, read off its `User-Agent`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Class {
    /// The AI crawlers robots.txt names: GPTBot, ClaudeBot, PerplexityBot,
    /// Google-Extended and their kin.
    AiCrawler,
    /// The classic search engines.
    SearchCrawler,
    /// Everybody else: browsers, scripts, unknown bots.
    Other,
}

/// The AI crawlers robots.txt speaks to by name.
pub const AI_CRAWLERS: &[&str] = &[
    "GPTBot",
    "ChatGPT-User",
    "OAI-SearchBot",
    "ClaudeBot",
    "Claude-User",
    "Claude-SearchBot",
    "anthropic-ai",
    "PerplexityBot",
    "Perplexity-User",
    "Google-Extended",
    "Applebot-Extended",
    "CCBot",
    "Meta-ExternalAgent",
    "Amazonbot",
    "DuckAssistBot",
    "MistralAI-User",
];

/// The classic search engines robots.txt speaks to by name.
pub const SEARCH_CRAWLERS: &[&str] = &["Googlebot", "Bingbot", "DuckDuckBot", "Applebot", "YandexBot", "Baiduspider", "SeznamBot"];

/// Which [`Class`] a `User-Agent` falls in.
pub fn classify(user_agent: &str) -> Class {
    let ua = user_agent.to_ascii_lowercase();
    if AI_CRAWLERS.iter().any(|b| ua.contains(&b.to_ascii_lowercase())) {
        Class::AiCrawler
    } else if SEARCH_CRAWLERS.iter().any(|b| ua.contains(&b.to_ascii_lowercase())) {
        Class::SearchCrawler
    } else {
        Class::Other
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_crawlers_are_told_apart_by_their_user_agent() {
        assert_eq!(classify("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.2; +https://openai.com/gptbot)"), Class::AiCrawler);
        assert_eq!(classify("Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)"), Class::AiCrawler);
        assert_eq!(classify("Mozilla/5.0 (compatible; PerplexityBot/1.0)"), Class::AiCrawler);
        assert_eq!(classify("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"), Class::SearchCrawler);
        assert_eq!(classify("Mozilla/5.0 (compatible; bingbot/2.0)"), Class::SearchCrawler);
        assert_eq!(classify("Mozilla/5.0 (X11; Linux x86_64) Firefox/131.0"), Class::Other);
        assert_eq!(classify(""), Class::Other);
    }
}