Skip to content

Commit

Permalink
feat: add telegram integration (#59)
Browse files Browse the repository at this point in the history
* add telegram integration

* address comments

* address comments

* fix check

* address comments
  • Loading branch information
cctdaniel authored May 9, 2024
1 parent beca870 commit 9803a22
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 13 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ __pycache__/

.DS_Store
.envrc
.coverage
.coverage

.env
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,18 @@ Event types are configured via environment variables:

- `LogEvent`
- `LOG_EVENT_LEVEL` - Level to log messages at

- `TelegramEvent`
- `TELEGRAM_BOT_TOKEN` - API token for the Telegram bot

## Finding the Telegram Group Chat ID

To integrate Telegram events with the Observer, you need the Telegram group chat ID. Here's how you can find it:

1. Open [Telegram Web](https://web.telegram.org).
2. Navigate to the group chat for which you need the ID.
3. Look at the URL in the browser's address bar; it should look something like `https://web.telegram.org/a/#-1111111111`.
4. The group chat ID is the number in the URL, including the `-` sign if present (e.g., `-1111111111`).

Use this ID in the `publishers.yaml` configuration to correctly set up Telegram events.

16 changes: 15 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pyyaml = "^6.0"
throttler = "1.2.1"
types-pyyaml = "^6.0.12"
types-pytz = "^2022.4.0.0"
python-dotenv = "^1.0.1"


[tool.poetry.group.dev.dependencies]
Expand Down
6 changes: 4 additions & 2 deletions pyth_observer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from pyth_observer.crosschain import CrosschainPrice
from pyth_observer.crosschain import CrosschainPriceObserver as Crosschain
from pyth_observer.dispatch import Dispatch
from pyth_observer.models import Publisher

PYTHTEST_HTTP_ENDPOINT = "https://api.pythtest.pyth.network/"
PYTHTEST_WS_ENDPOINT = "wss://api.pythtest.pyth.network/"
Expand Down Expand Up @@ -49,7 +50,7 @@ class Observer:
def __init__(
self,
config: Dict[str, Any],
publishers: Dict[str, str],
publishers: Dict[str, Publisher],
coingecko_mapping: Dict[str, Symbol],
):
self.config = config
Expand Down Expand Up @@ -134,8 +135,9 @@ async def run(self):
)

for component in price_account.price_components:
pub = self.publishers.get(component.publisher_key.key, None)
publisher_name = (
self.publishers.get(component.publisher_key.key, "")
(pub.name if pub else "")
+ f" ({component.publisher_key.key})"
).strip()
states.append(
Expand Down
25 changes: 21 additions & 4 deletions pyth_observer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from loguru import logger
from prometheus_client import start_http_server

from pyth_observer import Observer
from pyth_observer import Observer, Publisher
from pyth_observer.models import ContactInfo


@click.command()
Expand Down Expand Up @@ -37,10 +38,26 @@
)
def run(config, publishers, coingecko_mapping, prometheus_port):
config_ = yaml.safe_load(open(config, "r"))
publishers_ = yaml.safe_load(open(publishers, "r"))
publishers_inverted = {v: k for k, v in publishers_.items()}
# Load publishers YAML file and convert to dictionary of Publisher instances
publishers_raw = yaml.safe_load(open(publishers, "r"))
publishers_ = {
publisher["key"]: Publisher(
key=publisher["key"],
name=publisher["name"],
contact_info=(
ContactInfo(**publisher["contact_info"])
if "contact_info" in publisher
else None
),
)
for publisher in publishers_raw
}
coingecko_mapping_ = yaml.safe_load(open(coingecko_mapping, "r"))
observer = Observer(config_, publishers_inverted, coingecko_mapping_)
observer = Observer(
config_,
publishers_,
coingecko_mapping_,
)

start_http_server(int(prometheus_port))

Expand Down
2 changes: 2 additions & 0 deletions pyth_observer/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
from pyth_observer.check.publisher import PUBLISHER_CHECKS, PublisherState
from pyth_observer.event import DatadogEvent # Used dynamically
from pyth_observer.event import LogEvent # Used dynamically
from pyth_observer.event import TelegramEvent # Used dynamically
from pyth_observer.event import Event

assert DatadogEvent
assert LogEvent
assert TelegramEvent


class Dispatch:
Expand Down
51 changes: 50 additions & 1 deletion pyth_observer/event.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import os
from typing import Dict, Literal, Protocol, TypedDict, cast

import aiohttp
from datadog_api_client.api_client import AsyncApiClient as DatadogAPI
from datadog_api_client.configuration import Configuration as DatadogConfig
from datadog_api_client.v1.api.events_api import EventsApi as DatadogEventAPI
from datadog_api_client.v1.model.event_alert_type import EventAlertType
from datadog_api_client.v1.model.event_create_request import EventCreateRequest
from dotenv import load_dotenv
from loguru import logger

from pyth_observer.check import Check
from pyth_observer.check.publisher import PublisherCheck
from pyth_observer.models import Publisher

load_dotenv()


class Context(TypedDict):
network: str
publishers: Dict[str, str]
publishers: Dict[str, Publisher]


class Event(Protocol):
Expand Down Expand Up @@ -94,3 +99,47 @@ async def send(self):

level = cast(LogEventLevel, os.environ.get("LOG_EVENT_LEVEL", "INFO"))
logger.log(level, text.replace("\n", ". "))


class TelegramEvent(Event):
def __init__(self, check: Check, context: Context):
self.check = check
self.context = context
self.telegram_bot_token = os.environ["TELEGRAM_BOT_TOKEN"]

async def send(self):
if self.check.__class__.__bases__ == (PublisherCheck,):
text = self.check.error_message()
publisher_key = self.check.state().public_key.key
publisher = self.context["publishers"].get(publisher_key, None)
# Ensure publisher is not None and has contact_info before accessing telegram_chat_id
chat_id = (
publisher.contact_info.telegram_chat_id
if publisher is not None and publisher.contact_info is not None
else None
)

if chat_id is None:
logger.warning(
f"Telegram chat ID not found for publisher key {publisher_key}"
)
return

telegram_api_url = (
f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage"
)
message_data = {
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown",
}

async with aiohttp.ClientSession() as session:
async with session.post(
telegram_api_url, json=message_data
) as response:
if response.status != 200:
response_text = await response.text()
logger.error(
f"Failed to send Telegram message: {response_text}"
)
16 changes: 16 additions & 0 deletions pyth_observer/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import dataclasses
from typing import Optional


@dataclasses.dataclass
class ContactInfo:
telegram_chat_id: Optional[str] = None
email: Optional[str] = None
slack_channel_id: Optional[str] = None


@dataclasses.dataclass
class Publisher:
key: str
name: str
contact_info: Optional[ContactInfo] = None
1 change: 1 addition & 0 deletions sample.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ events:
# NOTE: Uncomment to enable Datadog metrics, see README.md for datadog credential docs.
# - DatadogEvent
- LogEvent
- TelegramEvent
checks:
global:
# Price feed checks
Expand Down
19 changes: 15 additions & 4 deletions sample.publishers.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
{
"publisher1": "66wJmrBqyykL7m4Erj4Ud29qhsm32DHSTo23zooupJrJ",
"publisher2": "3BkoB5MBSrrnDY7qe694UAuPpeMg7zJnodwbCnayNYzC",
}
- name: publisher1
key: "FR19oB2ePko2haah8yP4fhTycxitxkVQTxk3tssxX1Ce"
contact_info:
# Optional fields for contact information
telegram_chat_id: -4224704640
email:
slack_channel_id:

- name: publisher2
key: "DgAK7fPveidN72LCwCF4QjFcYHchBZbtZnjEAtgU1bMX"
contact_info:
# Optional fields for contact information
telegram_chat_id: -4224704640
email:
slack_channel_id:

0 comments on commit 9803a22

Please sign in to comment.