203 lines
6.8 KiB
Python
203 lines
6.8 KiB
Python
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
|