diff --git a/Gemfile b/Gemfile index db09839..b156e94 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,7 @@ gem 'redis', '>= 4.0.1' gem 'chartkick' gem 'devise' gem 'font-awesome-sass', '~> 6.5.2' -gem 'geocoder' +gem 'maxmind-geoip2' gem 'groupdate' gem 'mini_magick' gem 'sidekiq' diff --git a/Gemfile.lock b/Gemfile.lock index 4ac7bd9..68e51ac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,7 +113,6 @@ GEM concurrent-ruby (1.3.3) connection_pool (2.4.1) crass (1.0.6) - csv (3.3.0) date (3.3.4) debug (1.9.2) irb (~> 1.10) @@ -125,6 +124,7 @@ GEM responders warden (~> 1.2.3) diff-lcs (1.5.1) + domain_name (0.6.20240107) drb (2.2.1) dry-cli (1.1.0) erubi (1.13.0) @@ -141,18 +141,27 @@ GEM ffi (1.17.0-x86-linux-gnu) ffi (1.17.0-x86_64-darwin) ffi (1.17.0-x86_64-linux-gnu) + ffi-compiler (1.3.2) + ffi (>= 1.15.5) + rake font-awesome-sass (6.5.2) sassc (~> 2.0) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) - geocoder (1.8.3) - base64 (>= 0.1.0) - csv (>= 3.0.0) globalid (1.2.1) activesupport (>= 6.1) groupdate (6.4.0) activesupport (>= 6.1) + http (5.2.0) + addressable (~> 2.8) + base64 (~> 0.1) + http-cookie (~> 1.0) + http-form_data (~> 2.2) + llhttp-ffi (~> 0.5.0) + http-cookie (1.0.7) + domain_name (~> 0.5) + http-form_data (2.3.0) i18n (1.14.5) concurrent-ruby (~> 1.0) importmap-rails (2.0.1) @@ -167,6 +176,9 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) + llhttp-ffi (0.5.0) + ffi-compiler (~> 1.0) + rake (~> 13.0) logger (1.6.0) loofah (2.22.0) crass (~> 1.0.2) @@ -178,6 +190,11 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + maxmind-db (1.2.0) + maxmind-geoip2 (1.2.0) + connection_pool (~> 2.2) + http (>= 4.3, < 6.0) + maxmind-db (~> 1.2) mini_magick (5.0.1) mini_mime (1.1.5) minitest (5.24.1) @@ -385,10 +402,10 @@ DEPENDENCIES devise factory_bot_rails font-awesome-sass (~> 6.5.2) - geocoder groupdate importmap-rails jbuilder + maxmind-geoip2 mini_magick puma (>= 5.0) rails (~> 7.1.3, >= 7.1.3.4) diff --git a/README.md b/README.md index 72941b3..5abb7f1 100644 --- a/README.md +++ b/README.md @@ -286,16 +286,6 @@ The restore process: 2. Loads the specified backup file. 3. Applies any pending migrations. -## Geolocation - -Geolocation is currently a mandatory feature in Linkarooie. It uses the `geocoder` gem to provide location-based insights for link clicks and page views. - -To enable geolocation: -1. Obtain a free API key from [ipapi](https://ipapi.com). -2. Set the `GEOCODER_API_KEY` environment variable with your API key. - -Future plans include making geolocation optional to cater to different privacy preferences. - ## Customization Linkarooie is designed to be highly customizable: diff --git a/app/controllers/analytics_controller.rb b/app/controllers/analytics_controller.rb index cb827a2..a7db7c6 100644 --- a/app/controllers/analytics_controller.rb +++ b/app/controllers/analytics_controller.rb @@ -39,12 +39,12 @@ def fetch_cached_data(key, &block) def fetch_location_data @user.page_views.group(:country, :city).count.map do |location, count| { - country: location[0], - city: location[1], + country: location[0] || 'Unknown', # Fallback to 'Unknown' if no country + city: location[1] || 'Unknown', # Fallback to 'Unknown' if no city count: count } end.sort_by { |location| -location[:count] }.take(10) - end + end # Update this method to exclude hidden links def fetch_link_analytics diff --git a/app/middleware/page_view_tracker.rb b/app/middleware/page_view_tracker.rb index acc0cac..1a4e3f9 100644 --- a/app/middleware/page_view_tracker.rb +++ b/app/middleware/page_view_tracker.rb @@ -1,6 +1,12 @@ +require 'maxmind/geoip2' + class PageViewTracker def initialize(app) @app = app + # Load MaxMind readers for ASN, City, and Country databases + @asn_reader = MaxMind::GeoIP2::Reader.new('db/GeoLite2-ASN.mmdb') # ASN database + @city_reader = MaxMind::GeoIP2::Reader.new('db/GeoLite2-City.mmdb') # City database + @country_reader = MaxMind::GeoIP2::Reader.new('db/GeoLite2-Country.mmdb') # Country database end def call(env) @@ -25,10 +31,16 @@ def track_page_view(request) if user # Extract the original IP from the Cloudflare headers if available real_ip = request.headers['CF-Connecting-IP'] || request.headers['X-Forwarded-For']&.split(',')&.first || request.ip - - # Use Geocoder to find the location based on the real IP - location = Geocoder.search(real_ip).first - + + # ASN lookup + asn_record = @asn_reader.asn(real_ip) rescue nil + + # City lookup + city_record = @city_reader.city(real_ip) rescue nil + + # Country lookup (fallback if city is not found) + country_record = @country_reader.country(real_ip) rescue nil + PageView.create( user: user, path: request.path, @@ -37,18 +49,19 @@ def track_page_view(request) visited_at: Time.current, ip_address: real_ip, session_id: request.session[:session_id], - country: location&.country, - city: location&.city, - state: location&.state, - county: location&.county, - latitude: location&.latitude, - longitude: location&.longitude, - country_code: location&.country_code + country: city_record&.country&.iso_code || country_record&.country&.iso_code, + city: city_record&.city&.name, + state: city_record&.subdivisions&.first&.iso_code, + latitude: city_record&.location&.latitude, + longitude: city_record&.location&.longitude, + country_code: city_record&.country&.iso_code || country_record&.country&.iso_code, + asn: asn_record&.autonomous_system_number, + asn_org: asn_record&.autonomous_system_organization ) end rescue ActiveRecord::RecordNotUnique Rails.logger.info "Duplicate page view detected and ignored" rescue => e Rails.logger.error "Error tracking page view: #{e.message}" - end -end \ No newline at end of file + end +end diff --git a/config/initializers/geocoder.rb b/config/initializers/geocoder.rb deleted file mode 100644 index aeb30fc..0000000 --- a/config/initializers/geocoder.rb +++ /dev/null @@ -1,30 +0,0 @@ -Geocoder.configure( - # Geocoding options - timeout: 5, # geocoding service timeout (secs) - lookup: :google, # name of geocoding service (symbol) - ip_lookup: :ipinfo_io, # name of IP address geocoding service (you can choose a service) - language: :en, # ISO-639 language code - - # Use HTTPS for lookup requests - use_https: true, - - # API key for geocoding service - api_key: ENV['GEOCODER_API_KEY'], # Geocoder API key from environment variable - - # Caching - cache: Redis.new, # cache object (e.g. Redis.new) - cache_prefix: "geocoder:", # prefix for cache keys - - # Exceptions that should not be rescued by default - # (if you want to handle them yourself) - always_raise: [ - Geocoder::OverQueryLimitError, - Geocoder::RequestDenied, - Geocoder::InvalidRequest, - Geocoder::InvalidApiKey - ], - - # Calculation options - units: :km, # :km for kilometers or :mi for miles - distances: :linear # :spherical or :linear -) diff --git a/db/GeoLite2-ASN.mmdb b/db/GeoLite2-ASN.mmdb new file mode 100644 index 0000000..7b01677 Binary files /dev/null and b/db/GeoLite2-ASN.mmdb differ diff --git a/db/GeoLite2-City.mmdb b/db/GeoLite2-City.mmdb new file mode 100644 index 0000000..a9f2475 Binary files /dev/null and b/db/GeoLite2-City.mmdb differ diff --git a/db/GeoLite2-Country.mmdb b/db/GeoLite2-Country.mmdb new file mode 100644 index 0000000..bd93f84 Binary files /dev/null and b/db/GeoLite2-Country.mmdb differ diff --git a/db/migrate/20240914124058_add_asn_fields_to_page_views.rb b/db/migrate/20240914124058_add_asn_fields_to_page_views.rb new file mode 100644 index 0000000..9de1d82 --- /dev/null +++ b/db/migrate/20240914124058_add_asn_fields_to_page_views.rb @@ -0,0 +1,6 @@ +class AddAsnFieldsToPageViews < ActiveRecord::Migration[7.1] + def change + add_column :page_views, :asn, :string + add_column :page_views, :asn_org, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index a82ec1f..1bf4413 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_09_14_101600) do +ActiveRecord::Schema[7.1].define(version: 2024_09_14_124058) do create_table "achievement_views", force: :cascade do |t| t.integer "achievement_id", null: false t.integer "user_id", null: false @@ -123,6 +123,8 @@ t.float "latitude" t.float "longitude" t.string "country_code" + t.string "asn" + t.string "asn_org" t.index ["user_id"], name: "index_page_views_on_user_id" end