Skip to content

Commit 9803a22

Browse files
authored
feat: add telegram integration (#59)
* add telegram integration * address comments * address comments * fix check * address comments
1 parent beca870 commit 9803a22

File tree

11 files changed

+143
-13
lines changed

11 files changed

+143
-13
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ __pycache__/
44

55
.DS_Store
66
.envrc
7-
.coverage
7+
.coverage
8+
9+
.env

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,18 @@ Event types are configured via environment variables:
3030

3131
- `LogEvent`
3232
- `LOG_EVENT_LEVEL` - Level to log messages at
33+
34+
- `TelegramEvent`
35+
- `TELEGRAM_BOT_TOKEN` - API token for the Telegram bot
36+
37+
## Finding the Telegram Group Chat ID
38+
39+
To integrate Telegram events with the Observer, you need the Telegram group chat ID. Here's how you can find it:
40+
41+
1. Open [Telegram Web](https://web.telegram.org).
42+
2. Navigate to the group chat for which you need the ID.
43+
3. Look at the URL in the browser's address bar; it should look something like `https://web.telegram.org/a/#-1111111111`.
44+
4. The group chat ID is the number in the URL, including the `-` sign if present (e.g., `-1111111111`).
45+
46+
Use this ID in the `publishers.yaml` configuration to correctly set up Telegram events.
47+

poetry.lock

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pyyaml = "^6.0"
2626
throttler = "1.2.1"
2727
types-pyyaml = "^6.0.12"
2828
types-pytz = "^2022.4.0.0"
29+
python-dotenv = "^1.0.1"
2930

3031

3132
[tool.poetry.group.dev.dependencies]

pyth_observer/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from pyth_observer.crosschain import CrosschainPrice
2323
from pyth_observer.crosschain import CrosschainPriceObserver as Crosschain
2424
from pyth_observer.dispatch import Dispatch
25+
from pyth_observer.models import Publisher
2526

2627
PYTHTEST_HTTP_ENDPOINT = "https://api.pythtest.pyth.network/"
2728
PYTHTEST_WS_ENDPOINT = "wss://api.pythtest.pyth.network/"
@@ -49,7 +50,7 @@ class Observer:
4950
def __init__(
5051
self,
5152
config: Dict[str, Any],
52-
publishers: Dict[str, str],
53+
publishers: Dict[str, Publisher],
5354
coingecko_mapping: Dict[str, Symbol],
5455
):
5556
self.config = config
@@ -134,8 +135,9 @@ async def run(self):
134135
)
135136

136137
for component in price_account.price_components:
138+
pub = self.publishers.get(component.publisher_key.key, None)
137139
publisher_name = (
138-
self.publishers.get(component.publisher_key.key, "")
140+
(pub.name if pub else "")
139141
+ f" ({component.publisher_key.key})"
140142
).strip()
141143
states.append(

pyth_observer/cli.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from loguru import logger
88
from prometheus_client import start_http_server
99

10-
from pyth_observer import Observer
10+
from pyth_observer import Observer, Publisher
11+
from pyth_observer.models import ContactInfo
1112

1213

1314
@click.command()
@@ -37,10 +38,26 @@
3738
)
3839
def run(config, publishers, coingecko_mapping, prometheus_port):
3940
config_ = yaml.safe_load(open(config, "r"))
40-
publishers_ = yaml.safe_load(open(publishers, "r"))
41-
publishers_inverted = {v: k for k, v in publishers_.items()}
41+
# Load publishers YAML file and convert to dictionary of Publisher instances
42+
publishers_raw = yaml.safe_load(open(publishers, "r"))
43+
publishers_ = {
44+
publisher["key"]: Publisher(
45+
key=publisher["key"],
46+
name=publisher["name"],
47+
contact_info=(
48+
ContactInfo(**publisher["contact_info"])
49+
if "contact_info" in publisher
50+
else None
51+
),
52+
)
53+
for publisher in publishers_raw
54+
}
4255
coingecko_mapping_ = yaml.safe_load(open(coingecko_mapping, "r"))
43-
observer = Observer(config_, publishers_inverted, coingecko_mapping_)
56+
observer = Observer(
57+
config_,
58+
publishers_,
59+
coingecko_mapping_,
60+
)
4461

4562
start_http_server(int(prometheus_port))
4663

pyth_observer/dispatch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
from pyth_observer.check.publisher import PUBLISHER_CHECKS, PublisherState
1010
from pyth_observer.event import DatadogEvent # Used dynamically
1111
from pyth_observer.event import LogEvent # Used dynamically
12+
from pyth_observer.event import TelegramEvent # Used dynamically
1213
from pyth_observer.event import Event
1314

1415
assert DatadogEvent
1516
assert LogEvent
17+
assert TelegramEvent
1618

1719

1820
class Dispatch:

pyth_observer/event.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import os
22
from typing import Dict, Literal, Protocol, TypedDict, cast
33

4+
import aiohttp
45
from datadog_api_client.api_client import AsyncApiClient as DatadogAPI
56
from datadog_api_client.configuration import Configuration as DatadogConfig
67
from datadog_api_client.v1.api.events_api import EventsApi as DatadogEventAPI
78
from datadog_api_client.v1.model.event_alert_type import EventAlertType
89
from datadog_api_client.v1.model.event_create_request import EventCreateRequest
10+
from dotenv import load_dotenv
911
from loguru import logger
1012

1113
from pyth_observer.check import Check
1214
from pyth_observer.check.publisher import PublisherCheck
15+
from pyth_observer.models import Publisher
16+
17+
load_dotenv()
1318

1419

1520
class Context(TypedDict):
1621
network: str
17-
publishers: Dict[str, str]
22+
publishers: Dict[str, Publisher]
1823

1924

2025
class Event(Protocol):
@@ -94,3 +99,47 @@ async def send(self):
9499

95100
level = cast(LogEventLevel, os.environ.get("LOG_EVENT_LEVEL", "INFO"))
96101
logger.log(level, text.replace("\n", ". "))
102+
103+
104+
class TelegramEvent(Event):
105+
def __init__(self, check: Check, context: Context):
106+
self.check = check
107+
self.context = context
108+
self.telegram_bot_token = os.environ["TELEGRAM_BOT_TOKEN"]
109+
110+
async def send(self):
111+
if self.check.__class__.__bases__ == (PublisherCheck,):
112+
text = self.check.error_message()
113+
publisher_key = self.check.state().public_key.key
114+
publisher = self.context["publishers"].get(publisher_key, None)
115+
# Ensure publisher is not None and has contact_info before accessing telegram_chat_id
116+
chat_id = (
117+
publisher.contact_info.telegram_chat_id
118+
if publisher is not None and publisher.contact_info is not None
119+
else None
120+
)
121+
122+
if chat_id is None:
123+
logger.warning(
124+
f"Telegram chat ID not found for publisher key {publisher_key}"
125+
)
126+
return
127+
128+
telegram_api_url = (
129+
f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage"
130+
)
131+
message_data = {
132+
"chat_id": chat_id,
133+
"text": text,
134+
"parse_mode": "Markdown",
135+
}
136+
137+
async with aiohttp.ClientSession() as session:
138+
async with session.post(
139+
telegram_api_url, json=message_data
140+
) as response:
141+
if response.status != 200:
142+
response_text = await response.text()
143+
logger.error(
144+
f"Failed to send Telegram message: {response_text}"
145+
)

pyth_observer/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import dataclasses
2+
from typing import Optional
3+
4+
5+
@dataclasses.dataclass
6+
class ContactInfo:
7+
telegram_chat_id: Optional[str] = None
8+
email: Optional[str] = None
9+
slack_channel_id: Optional[str] = None
10+
11+
12+
@dataclasses.dataclass
13+
class Publisher:
14+
key: str
15+
name: str
16+
contact_info: Optional[ContactInfo] = None

sample.config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ events:
1010
# NOTE: Uncomment to enable Datadog metrics, see README.md for datadog credential docs.
1111
# - DatadogEvent
1212
- LogEvent
13+
- TelegramEvent
1314
checks:
1415
global:
1516
# Price feed checks

0 commit comments

Comments
 (0)