from dataclasses import dataclass from typing import List, Any, Optional from enum import Enum import re class EventType(Enum): """Enumeration of possible Ingress event types. Attributes: RESONATOR_DEPLOYED: A resonator was deployed on a portal. RESONATOR_DESTROYED: A resonator was destroyed on a portal. PORTAL_CAPTURED: A portal was captured by a faction. PORTAL_NEUTRALIZED: A portal was neutralized. PORTAL_UNDER_ATTACK: A portal is under attack. LINK_CREATED: A link was created between portals. LINK_DESTROYED: A link was destroyed. CONTROL_FIELD_CREATED: A control field was created. UNKNOWN: Unknown event type. """ RESONATOR_DEPLOYED = "RESONATOR_DEPLOYED" RESONATOR_DESTROYED = "RESONATOR_DESTROYED" PORTAL_CAPTURED = "PORTAL_CAPTURED" PORTAL_NEUTRALIZED = "PORTAL_NEUTRALIZED" PORTAL_UNDER_ATTACK = "PORTAL_UNDER_ATTACK" LINK_CREATED = "LINK_CREATED" LINK_DESTROYED = "LINK_DESTROYED" CONTROL_FIELD_CREATED = "CONTROL_FIELD_CREATED" UNKNOWN = "UNKNOWN" EVENT_TYPE_KEYWORDS = { EventType.RESONATOR_DEPLOYED: ["deployed a Resonator on"], EventType.RESONATOR_DESTROYED: ["destroyed a Resonator on"], EventType.LINK_DESTROYED: ["destroyed the", "Link"], EventType.PORTAL_CAPTURED: ["captured"], EventType.PORTAL_NEUTRALIZED: ["neutralized by"], EventType.PORTAL_UNDER_ATTACK: ["is under attack by"], EventType.LINK_CREATED: ["linked from"], EventType.CONTROL_FIELD_CREATED: ["created a Control Field"], } @dataclass class Markup: """Represents markup data within a plext message. Attributes: type: The type of markup (e.g., "PLAYER", "PORTAL"). plain: Plain text representation of the markup. team: Team affiliation (e.g., "RESISTANCE", "ENLIGHTENED"). name: Name associated with the markup (e.g., player name, portal name). address: Address of the location (for portals). latE6: Latitude in microdegrees (E6 format). lngE6: Longitude in microdegrees (E6 format). """ type: str plain: str team: str = "" name: str = "" address: str = "" latE6: int = 0 lngE6: int = 0 @dataclass class Plext: """Represents a plext (message) from the Ingress Intel API. Attributes: id: Unique identifier for the plext. timestamp: Timestamp in milliseconds since epoch. text: The text content of the plext. team: Team affiliation (e.g., "RESISTANCE", "ENLIGHTENED"). plext_type: Type of plext (e.g., "SYSTEM_BROADCAST", "PLAYER_GENERATED"). categories: Category flags for the plext. markup: List of Markup objects containing structured data. """ id: str timestamp: int text: str team: str plext_type: str categories: int markup: List[Markup] @classmethod def from_json(cls, data: List[Any]) -> "Plext": """Create a Plext instance from raw JSON data. Args: data: Raw JSON data from the Ingress API. Expected format is a list where index 2 contains a dict with a "plext" key. Returns: A new Plext instance populated with data from the JSON. """ plext_data = data[2]["plext"] markup_data = plext_data["markup"] markup = [] for m in markup_data: markup_type = m[0] markup_details = m[1] markup.append( Markup( type=markup_type, plain=markup_details.get("plain", ""), team=markup_details.get("team", ""), name=markup_details.get("name", ""), address=markup_details.get("address", ""), latE6=markup_details.get("latE6", 0), lngE6=markup_details.get("lngE6", 0), ) ) return cls( id=data[0], timestamp=data[1], text=plext_data["text"], team=plext_data["team"], plext_type=plext_data["plextType"], categories=plext_data["categories"], markup=markup, ) def get_event_type(self) -> EventType: """Determine the event type based on the plext text content. Returns: The EventType that matches the plext text, or EventType.UNKNOWN if no match is found. """ for event_type, keywords in EVENT_TYPE_KEYWORDS.items(): if all(keyword in self.text for keyword in keywords): # A special case for "captured", to avoid matching "destroyed" if event_type == EventType.PORTAL_CAPTURED and "destroyed" in self.text: continue return event_type return EventType.UNKNOWN def get_player_name(self) -> str: """Extract the player name from the plext. First attempts to find the player name in the markup. If not found, attempts to extract it from the text using regex. Returns: The player name if found, otherwise an empty string. """ for m in self.markup: if m.type == "PLAYER": return m.plain # If player name is not in markup, try to extract from text if "agent" in self.text: match = re.search(r"agent (\w+)", self.text) if match: return match.group(1) return "" def get_portal_name(self) -> str: """Extract the portal name from the plext. First attempts to find the portal name in the markup. If not found, attempts to extract it from the text using regex patterns for various event types. Returns: The portal name if found, otherwise an empty string. """ for m in self.markup: if m.type == "PORTAL": return m.name # If portal name is not in markup, try to extract from text match = re.search( r"(?:deployed|destroyed|captured|linked from|created a Control Field @) (.+?) \(", self.text, ) if not match: match = re.search(r"Your Portal (.+?) is under attack by", self.text) if not match: match = re.search(r"Your Portal (.+?) neutralized by", self.text) if match: return match.group(1).strip() return "" def get_event_coordinates(self) -> Optional[tuple[int, int]]: """Extract the coordinates of the portal associated with this event. Searches the markup for a PORTAL type entry and returns its coordinates. Returns: A tuple of (latitude, longitude) in microdegrees (E6 format) if found, otherwise None. """ for m in self.markup: if m.type == "PORTAL": return m.latE6, m.lngE6 return None