defmodule Plausible.Geo do @moduledoc """ This module provides an API for fetching IP geolocation. """ require Logger @db :geolocation @doc """ Starts the geodatabase loading process. Two modes are supported: local file and MaxMind license key. ## Options * `:path` - the path to the .mmdb database local file. When present, `:license_key` and `:edition` are not required. * `:license_key` - the [license key](https://support.maxmind.com/hc/en-us/articles/4407111582235-Generate-a-License-Key) from MaxMind to authenticate requests to MaxMind. * `:edition` - the name of the MaxMind database to be downloaded from MaxMind servers. Defaults to `GeoLite2-City`. * `:cache_dir` - if set, the downloaded .mmdb files are cached there across restarts. * `:async` - when used, configures the database loading to run asynchronously. ## Examples Loading from a local file: iex> load_db(path: "/etc/plausible/dbip-city.mmdb") :ok Downloading a MaxMind DB (this license key is no longer active): iex> load_db(license_key: "LNpsJCCKPis6XvBP", edition: "GeoLite2-City", async: true) :ok """ def load_db(opts) do cond do license_key = opts[:license_key] -> edition = opts[:edition] || "GeoLite2-City" maxmind_opts = [license_key: license_key] loader_opts = if is_binary(opts[:cache_dir]) do [ database_cache_file: String.to_charlist(Path.join(opts[:cache_dir], edition <> ".mmdb.gz")) ] else [:no_cache] end :ok = :locus.start_loader(@db, {:maxmind, edition}, maxmind_opts ++ loader_opts) path = opts[:path] -> :ok = :locus.start_loader(@db, path) true -> raise "failed to load geolocation db: need :path or :license_key to be provided" end unless opts[:async] do {:ok, _version} = :locus.await_loader(@db) end :ok end @doc """ Waits for the database to start after calling `load_db/1` with the async option. """ def await_loader, do: :locus.await_loader(@db) @doc """ Returns geodatabase type. Used for deciding whether to show the DB-IP disclaimer or not. ## Examples In the case of a DB-IP database: iex> database_type() "DBIP-City-Lite" In the case of a MaxMind database: iex> database_type() "GeoLite2-City" """ def database_type do case :locus.get_info(@db, :metadata) do {:ok, %{database_type: type}} -> type _other -> nil end end @doc """ Looks up geo info about an IP address. ## Examples iex> lookup("8.7.6.5") %{ "city" => %{ "geoname_id" => 5349755, "names" => %{ "de" => "Fontana", "en" => "Fontana", "ja" => "フォンタナ", "ru" => "Фонтана" } }, "continent" => %{ "code" => "NA", "geoname_id" => 6255149, "names" => %{ "de" => "Nordamerika", "en" => "North America", "es" => "Norteamérica", "fr" => "Amérique du Nord", "ja" => "北アメリカ", "pt-BR" => "América do Norte", "ru" => "Северная Америка", "zh-CN" => "北美洲" } }, "country" => %{ "geoname_id" => 6252001, "iso_code" => "US", "names" => %{ "de" => "Vereinigte Staaten", "en" => "United States", "es" => "Estados Unidos", "fr" => "États Unis", "ja" => "アメリカ", "pt-BR" => "EUA", "ru" => "США", "zh-CN" => "美国" } }, "location" => %{ "accuracy_radius" => 50, "latitude" => 34.1211, "longitude" => -117.4362, "metro_code" => 803, "time_zone" => "America/Los_Angeles" }, "postal" => %{"code" => "92336"}, "registered_country" => %{ "geoname_id" => 6252001, "iso_code" => "US", "names" => %{ "de" => "Vereinigte Staaten", "en" => "United States", "es" => "Estados Unidos", "fr" => "États Unis", "ja" => "アメリカ", "pt-BR" => "EUA", "ru" => "США", "zh-CN" => "美国" } }, "subdivisions" => [ %{ "geoname_id" => 5332921, "iso_code" => "CA", "names" => %{ "de" => "Kalifornien", "en" => "California", "es" => "California", "fr" => "Californie", "ja" => "カリフォルニア州", "pt-BR" => "Califórnia", "ru" => "Калифорния", "zh-CN" => "加州" } } ] } """ def lookup(ip_address) do case :locus.lookup(@db, ip_address) do {:ok, entry} -> entry :not_found -> nil {:error, {:invalid_address, _address}} -> nil {:error, reason} -> Logger.error("Failed to lookup IP address. Reason: " <> inspect(reason)) nil end end end