Analytics!
This commit is contained in:
		
							parent
							
								
									485870a6b1
								
							
						
					
					
						commit
						a062934369
					
				
					 3 changed files with 112 additions and 9 deletions
				
			
		| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "locat"
 | 
			
		||||
version = "0.3.1"
 | 
			
		||||
version = "0.4.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
publish = ["menteeth"]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -8,4 +8,5 @@ publish = ["menteeth"]
 | 
			
		|||
 | 
			
		||||
[dependencies]
 | 
			
		||||
maxminddb = "0.23"
 | 
			
		||||
rusqlite = "0.28"
 | 
			
		||||
thiserror = "1"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@
 | 
			
		|||
      {
 | 
			
		||||
        defaultPackage = naersk-lib.buildPackage ./.;
 | 
			
		||||
        devShell = with pkgs; mkShell {
 | 
			
		||||
          buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy ];
 | 
			
		||||
          buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy sqlite];
 | 
			
		||||
          RUST_SRC_PATH = rustPlatform.rustLibSrc;
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										116
									
								
								src/lib.rs
									
										
									
									
									
								
							
							
						
						
									
										116
									
								
								src/lib.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -2,35 +2,137 @@ use std::net::IpAddr;
 | 
			
		|||
 | 
			
		||||
/// Allows geo-locating IPs and keeps analytics.
 | 
			
		||||
pub struct Locat {
 | 
			
		||||
    geoip: maxminddb::Reader<Vec<u8>>,
 | 
			
		||||
    reader: maxminddb::Reader<Vec<u8>>,
 | 
			
		||||
    analytics: Db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, thiserror::Error)]
 | 
			
		||||
pub enum Error {
 | 
			
		||||
    #[error("maxminddb error: {0}")]
 | 
			
		||||
    MaxMindDb(#[from] maxminddb::MaxMindDBError),
 | 
			
		||||
 | 
			
		||||
    #[error("rusqlite error: {0}")]
 | 
			
		||||
    Rusqlite(#[from] rusqlite::Error),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Locat {
 | 
			
		||||
    pub fn new(geoip_country_db_path: &str, _analytics_db_path: &str) -> Result<Self, Error> {
 | 
			
		||||
    pub fn new(geoip_country_db_path: &str, analytics_db_path: &str) -> Result<Self, Error> {
 | 
			
		||||
        // Todo: create analytics db.
 | 
			
		||||
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            geoip: maxminddb::Reader::open_readfile(geoip_country_db_path)?,
 | 
			
		||||
            reader: maxminddb::Reader::open_readfile(geoip_country_db_path)?,
 | 
			
		||||
            analytics: Db {
 | 
			
		||||
                path: analytics_db_path.to_string(),
 | 
			
		||||
            },
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Converts an address to an ISO 3166-1 alpha-2 country code.
 | 
			
		||||
    pub async fn ip_to_iso_code(&self, addr: IpAddr) -> Option<&str> {
 | 
			
		||||
        self.geoip
 | 
			
		||||
        let iso_code = self
 | 
			
		||||
            .reader
 | 
			
		||||
            .lookup::<maxminddb::geoip2::Country>(addr)
 | 
			
		||||
            .ok()?
 | 
			
		||||
            .country?
 | 
			
		||||
            .iso_code
 | 
			
		||||
            .iso_code?;
 | 
			
		||||
 | 
			
		||||
        if let Err(e) = self.analytics.increment(iso_code) {
 | 
			
		||||
            eprintln!("Could not increment analytic: {e}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Some(iso_code)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Returns a map of country codes to number of requests.
 | 
			
		||||
    pub async fn get_analytics(&self) -> Vec<(String, u64)> {
 | 
			
		||||
        Default::default()
 | 
			
		||||
    pub async fn get_analytics(&self) -> Result<Vec<(String, u64)>, Error> {
 | 
			
		||||
        Ok(self.analytics.list()?)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct Db {
 | 
			
		||||
    path: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Db {
 | 
			
		||||
    fn list(&self) -> Result<Vec<(String, u64)>, rusqlite::Error> {
 | 
			
		||||
        let conn = self.get_conn()?;
 | 
			
		||||
        let mut stmt = conn.prepare("SELECT iso_code, count FROM analytics")?;
 | 
			
		||||
        let mut rows = stmt.query([])?;
 | 
			
		||||
        let mut analytics = Vec::new();
 | 
			
		||||
        while let Some(row) = rows.next()? {
 | 
			
		||||
            let iso_code: String = row.get(0)?;
 | 
			
		||||
            let count: u64 = row.get(1)?;
 | 
			
		||||
            analytics.push((iso_code, count));
 | 
			
		||||
        }
 | 
			
		||||
        Ok(analytics)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn increment(&self, iso_code: &str) -> Result<(), rusqlite::Error> {
 | 
			
		||||
        let conn = self.get_conn().unwrap();
 | 
			
		||||
        let mut stmt = conn
 | 
			
		||||
            .prepare("INSERT INTO analytics (iso_code, count) VALUES (?, 1) ON CONFLICT (iso_code) DO UPDATE SET count = count + 1")?;
 | 
			
		||||
        stmt.execute([iso_code])?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_conn(&self) -> Result<rusqlite::Connection, rusqlite::Error> {
 | 
			
		||||
        let conn = rusqlite::Connection::open(&self.path).unwrap();
 | 
			
		||||
        self.migrate(&conn)?;
 | 
			
		||||
        Ok(conn)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn migrate(&self, conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
 | 
			
		||||
        // create analytics table.
 | 
			
		||||
        conn.execute(
 | 
			
		||||
            "CREATE  TABLE IF NOT EXISTS analytics (
 | 
			
		||||
                iso_code TEXT PRIMARY KEY,
 | 
			
		||||
                count INTEGER NOT NULL
 | 
			
		||||
            )",
 | 
			
		||||
            [],
 | 
			
		||||
        )?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use crate::Db;
 | 
			
		||||
 | 
			
		||||
    struct RemoveOnDrop {
 | 
			
		||||
        path: String,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl Drop for RemoveOnDrop {
 | 
			
		||||
        fn drop(&mut self) {
 | 
			
		||||
            _ = std::fs::remove_file(&self.path);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_db() {
 | 
			
		||||
        let db = Db {
 | 
			
		||||
            path: "/tmp/locat-test.db".to_string(),
 | 
			
		||||
        };
 | 
			
		||||
        let _remove_on_drop = RemoveOnDrop {
 | 
			
		||||
            path: db.path.clone(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let analytics = db.list().unwrap();
 | 
			
		||||
        assert_eq!(analytics.len(), 0);
 | 
			
		||||
 | 
			
		||||
        db.increment("US").unwrap();
 | 
			
		||||
        let analytics = db.list().unwrap();
 | 
			
		||||
        assert_eq!(analytics.len(), 1);
 | 
			
		||||
 | 
			
		||||
        db.increment("US").unwrap();
 | 
			
		||||
        db.increment("FR").unwrap();
 | 
			
		||||
        let analytics = db.list().unwrap();
 | 
			
		||||
        assert_eq!(analytics.len(), 2);
 | 
			
		||||
        // contains US at count 2
 | 
			
		||||
        assert!(analytics.contains(&("US".to_string(), 2)));
 | 
			
		||||
        // contains FR at count 1
 | 
			
		||||
        assert!(analytics.contains(&("FR".to_string(), 1)));
 | 
			
		||||
        // does not contain DE
 | 
			
		||||
        assert!(!analytics.contains(&("DE".to_string(), 0)));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue