import os from flask import Flask, request, jsonify from flask_cors import CORS from ingress import IngressAPI from models import EventType from pymongo import MongoClient from pymongo.errors import PyMongoError from dotenv import load_dotenv from lib import ( check_basic_auth, basic_auth_required, parse_timestamp, plext_to_dict, logger, ) load_dotenv() app = Flask(__name__) CORS(app) @app.route("/plexts/from-db", methods=["GET"]) @basic_auth_required def get_plexts_from_db(): """ Get plexts from MongoDB with optional filters. Returns plexts sorted by timestamp (most recent first). Query Parameters: player_name: Filter by player name (optional) timestamp_from: Minimum timestamp in milliseconds (optional) timestamp_to: Maximum timestamp in milliseconds (optional) limit: Maximum number of results to return (optional, default: 100) center: Geographic center point in format "latitude,longitude" (optional, requires radius) radius: Search radius in meters (optional, requires center) Returns: JSON response with list of plexts (without _id field), limited by the limit parameter. When using geospatial query (center and radius), includes a 'distance' field in meters. """ try: # Parse query parameters player_name = request.args.get("player_name") timestamp_from = request.args.get("timestamp_from") timestamp_to = request.args.get("timestamp_to") limit_param = request.args.get("limit") center = request.args.get("center") radius = request.args.get("radius") # Validate and convert timestamp parameters to integers if provided if timestamp_from is not None: try: timestamp_from = int(timestamp_from) except ValueError: return jsonify({"error": "timestamp_from must be an integer"}), 400 if timestamp_to is not None: try: timestamp_to = int(timestamp_to) except ValueError: return jsonify({"error": "timestamp_to must be an integer"}), 400 # Validate and convert limit parameter to integer if provided, otherwise default to 100 if limit_param is not None: try: limit = int(limit_param) if limit <= 0: return jsonify({"error": "limit must be a positive integer"}), 400 except ValueError: return jsonify({"error": "limit must be an integer"}), 400 else: limit = 100 # Validate geospatial parameters # Both center and radius must be provided together, or neither if (center is not None) != (radius is not None): return jsonify({ "error": "Both 'center' and 'radius' parameters must be provided together" }), 400 # Parse and validate center parameter if provided center_coords = None if center is not None: try: # Parse center in format "latitude,longitude" lat_str, lng_str = center.split(',') lat = float(lat_str) lng = float(lng_str) # Validate latitude and longitude ranges if not (-90 <= lat <= 90): return jsonify({"error": "Latitude must be between -90 and 90"}), 400 if not (-180 <= lng <= 180): return jsonify({"error": "Longitude must be between -180 and 180"}), 400 center_coords = (lat, lng) except ValueError: return jsonify({ "error": "Invalid center format. Use format: 'latitude,longitude' (e.g., '45.573661,12.365208')" }), 400 # Parse and validate radius parameter if provided radius_meters = None if radius is not None: try: radius_meters = int(radius) if radius_meters <= 0: return jsonify({"error": "Radius must be a positive integer"}), 400 except ValueError: return jsonify({"error": "Radius must be an integer"}), 400 # Build MongoDB filter query filter_query = {} if player_name: filter_query["player_name"] = player_name if timestamp_from is not None: filter_query["timestamp"] = filter_query.get("timestamp", {}) filter_query["timestamp"]["$gte"] = timestamp_from if timestamp_to is not None: filter_query["timestamp"] = filter_query.get("timestamp", {}) filter_query["timestamp"]["$lte"] = timestamp_to # Connect to MongoDB mongo_uri = os.getenv("MONGO_URI") db_name = os.getenv("DB_NAME") collection_name = os.getenv("COLLECTION_NAME") client = MongoClient(mongo_uri) db = client[db_name] collection = db[collection_name] try: # Ensure geospatial index exists try: collection.create_index([("coordinates", "2dsphere")], background=True) except PyMongoError as e: logger.warning(f"Could not create geospatial index: {e}") # Execute query based on whether geospatial parameters are provided if center_coords and radius_meters: # Use $geoNear aggregation for geospatial query lat, lng = center_coords pipeline = [ { "$geoNear": { "near": { "type": "Point", "coordinates": [lng, lat] # GeoJSON uses [longitude, latitude] }, "distanceField": "distance", # Add distance to results "maxDistance": radius_meters, # in meters "spherical": True # Use spherical geometry } }, { "$match": filter_query # Apply other filters }, { "$sort": {"timestamp": -1} # Sort by timestamp (most recent first) }, { "$limit": limit }, { "$project": { "_id": 0 # Exclude _id field } } ] # Execute aggregation pipeline plexts = list(collection.aggregate(pipeline)) else: # Use standard find() query for non-geospatial queries projection = {"_id": 0} # Execute query with sorting (timestamp DESC - most recent first) and dynamic limit cursor = ( collection.find(filter=filter_query, projection=projection) .sort("timestamp", -1) .limit(limit) ) # Convert cursor to list plexts = list(cursor) return jsonify({"count": len(plexts), "plexts": plexts}), 200 except PyMongoError as e: logger.error(f"MongoDB error: {e}") return jsonify({"error": "Database error"}), 500 finally: client.close() except Exception as e: logger.exception("Unexpected error in get_plexts_from_db") return jsonify({"error": "An error occurred"}), 500 @app.route("/plexts/from-api", methods=["GET"]) @basic_auth_required def get_plexts_from_api(): """ Get plexts from the Ingress API. Query Parameters: event_types: List of event types to filter by (comma-separated) player_name: Player name to filter by min_lat: Minimum latitude (default: 45470259) min_lng: Minimum longitude (default: 12244155) max_lat: Maximum latitude (default: 45480370) max_lng: Maximum longitude (default: 12298207) min_timestamp: Minimum timestamp (milliseconds or ISO 8601 format, default: -1) max_timestamp: Maximum timestamp (milliseconds or ISO 8601 format, default: -1) Returns: JSON response with list of plexts """ try: # Parse query parameters event_types_param = request.args.get("event_types") event_types = None if event_types_param: event_types_list = [et.strip() for et in event_types_param.split(",")] try: event_types = [EventType[et] for et in event_types_list] except KeyError as e: return jsonify({"error": f"Invalid event type: {e}"}), 400 player_name = request.args.get("player_name") min_lat = int(request.args.get("min_lat", os.getenv("MIN_LAT"))) min_lng = int(request.args.get("min_lng", os.getenv("MIN_LNG"))) max_lat = int(request.args.get("max_lat", os.getenv("MAX_LAT"))) max_lng = int(request.args.get("max_lng", os.getenv("MAX_LNG"))) min_timestamp_param = request.args.get("min_timestamp", "-1") max_timestamp_param = request.args.get("max_timestamp", "-1") min_timestamp = parse_timestamp(min_timestamp_param) max_timestamp = parse_timestamp(max_timestamp_param) # Initialize IngressAPI client cookie = os.getenv("INGRESS_COOKIE") client = IngressAPI( version=os.getenv("V"), cookie=cookie, ) # Fetch plexts plexts = client.get_plexts( min_lat_e6=min_lat, min_lng_e6=min_lng, max_lat_e6=max_lat, max_lng_e6=max_lng, min_timestamp_ms=min_timestamp, max_timestamp_ms=max_timestamp, event_types=event_types, player_name=player_name, ) # Sort plexts by timestamp (ascending - oldest first) sorted_plexts = sorted(plexts, key=lambda p: p.timestamp) # Convert to JSON-serializable format result = [plext_to_dict(p) for p in sorted_plexts] return jsonify( { "count": len(result), "plexts": result, } ) except ValueError as e: logger.warning(f"Validation error: {e}") return jsonify({"error": str(e)}), 400 except Exception as e: logger.exception("Error processing request") return jsonify({"error": f"An error occurred: {e}"}), 500 @app.route("/", methods=["GET"]) def index(): """Root endpoint providing API information. Returns: JSON response containing API name, version, and available endpoints with their parameters and descriptions. """ return jsonify( { "name": "Ingress Intel API", "version": "1.0.0", "endpoints": { "/plexts/from-api": { "method": "GET", "description": "Get plexts from the Ingress API", "parameters": { "event_types": "List of event types to filter by (comma-separated)", "player_name": "Player name to filter by", "min_lat": "Minimum latitude (default: 45470259)", "min_lng": "Minimum longitude (default: 12244155)", "max_lat": "Maximum latitude (default: 45480370)", "max_lng": "Maximum longitude (default: 12298207)", "min_timestamp": "Minimum timestamp (milliseconds or ISO 8601 format, default: -1)", "max_timestamp": "Maximum timestamp (milliseconds or ISO 8601 format, default: -1)", }, "event_types": [e.name for e in EventType], }, "/plexts/from-db": { "method": "GET", "description": "Get plexts from MongoDB with optional filters. Returns plexts sorted by timestamp (most recent first).", "parameters": { "player_name": "Filter by player name (optional)", "timestamp_from": "Minimum timestamp in milliseconds (optional)", "timestamp_to": "Maximum timestamp in milliseconds (optional)", "limit": "Maximum number of results to return (optional, default: 100)", "center": "Geographic center point in format 'latitude,longitude' (optional, requires radius)", "radius": "Search radius in meters (optional, requires center)", }, "authentication": "Basic Auth required", "geospatial_query": { "description": "When both center and radius are provided, returns plexts within the specified radius from the center point. If neither is provided, returns all plexts matching other filters.", "example": "center=45.573661,12.365208&radius=1000", "response_field": "distance (meters) - included in results when using geospatial query", }, }, }, } ) if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=os.getenv("PORT", 5000))