Files
Ingress/models.py
Matteo Rosati 46ddcc16da dockerize!
2026-01-14 23:54:51 +01:00

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