Skip to content

Commit

Permalink
Merge pull request #69 from aviate-labs/62-implement-telegrambot
Browse files Browse the repository at this point in the history
62 implement telegrambot
  • Loading branch information
mourginakis authored Oct 19, 2023
2 parents af00e4c + 3c1e54d commit 8751c7a
Show file tree
Hide file tree
Showing 12 changed files with 177 additions and 73 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ EMAIL_PASSWORD = "example_password"
# Tokens
TOKEN_DISCORD = "discord_token_example"
TOKEN_SLACK = "xoxb-123456789012-abcdefghijklmnopqrstuvwx"
TOKEN_TELEGRAM = "1234567890:ABC1dEF2gHIJk3456lM7_8no9pQrsTUVWXY"

# DB Info
DB_HOST=example_db_host
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added feature `node_status_report` to send a once-daily node status report to each user.
- Added the **Slack Bot** to be able to send messages through a slack channel.
- Added a scheduler to dispatch Node Status Reports at a certain time every day.
- Added the **Telegram Bot** to be able to send messages through a telegram chat.
- Deprecated fields from `NodeProviderDB`:
- `notify_telegram_channel` from the `subscribers` table.
- `telegram_channel_id` from the `channel_lookup` table.


## [1.0.0-alpha] - 2023-09-01
Expand Down
12 changes: 0 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,3 @@ $ rsync -anv --exclude '.git/' . username@remote_host:/root/directory

#### 2FA Gmail
If you are using gmail, you may need to create an ['app password'](https://support.google.com/mail/answer/185833)

#### Setting up the Slack Bot
1. Follow the steps in [this walkthrough](https://app.tango.us/app/workflow/Setting-up-a-Node-Monitor-Bot-in-Slack--Step-by-Step-Instructions-c971a31e13a344dc8cba4c2ebc3f4e4e), and keep track of the bot API token and the channel name.
2. Update the `TOKEN_SLACK` field in `.env` with the bot API token.
3. Update your database with any necessary channel information.

#### Setting up the Telegram Bot
- Not Yet Implemented
1. Follow the steps in this [walkthrough](https://help.nethunt.com/en/articles/6467726-how-to-create-a-telegram-bot-and-use-it-to-post-in-telegram-channels)
2. Update the `TOKEN_TELEGRAM` field in the `.env` file with the bot API token.
3. Update your database with any necessary channel information (for messaging a channel)
4. Update your database with the chat ID (for messaging a chat)
4 changes: 3 additions & 1 deletion node_monitor/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from node_monitor.bot_email import EmailBot
from node_monitor.bot_slack import SlackBot
from node_monitor.bot_telegram import TelegramBot
from node_monitor.node_monitor import NodeMonitor
from node_monitor.node_provider_db import NodeProviderDB
from node_monitor.server import create_server
Expand All @@ -13,9 +14,10 @@
## instance and work on the same data in different functions/threads
email_bot = EmailBot(c.EMAIL_USERNAME, c.EMAIL_PASSWORD)
slack_bot = SlackBot(c.TOKEN_SLACK)
telegram_bot = TelegramBot(c.TOKEN_TELEGRAM)
node_provider_db = NodeProviderDB(
c.DB_HOST, c.DB_NAME, c.DB_USERNAME, c.DB_PASSWORD, c.DB_PORT)
nm = NodeMonitor(email_bot, slack_bot, node_provider_db)
nm = NodeMonitor(node_provider_db, email_bot, slack_bot, telegram_bot)


## Run NodeMonitor in a separate thread
Expand Down
19 changes: 14 additions & 5 deletions node_monitor/bot_telegram.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import requests


class TelegramBot:
def __init__(self, telegram_token: str) -> None:
self.telegram_token = telegram_token

def send_message_to_channel(self) -> None:
pass

def send_message_to_chat(self) -> None:
pass
def send_message(
self, chat_id: str, message: str
) -> None | requests.exceptions.HTTPError:
try:
request = requests.get(
f"https://api.telegram.org/bot{self.telegram_token}"
f"/sendMessage?chat_id={chat_id}&text={message}"
)
request.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"Got an error: {e}")
return e
return None
20 changes: 11 additions & 9 deletions node_monitor/load_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
##############################################
## Secrets

EMAIL_USERNAME = os.environ.get('EMAIL_USERNAME', '')
EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD', '')
TOKEN_DISCORD = os.environ.get('TOKEN_DISCORD', '') # Not implemented
TOKEN_SLACK = os.environ.get('TOKEN_SLACK', '')
DB_HOST = os.environ.get('DB_HOST', '')
DB_USERNAME = os.environ.get('DB_USERNAME', '')
DB_PASSWORD = os.environ.get('DB_PASSWORD', '')
DB_NAME = os.environ.get('DB_NAME', '')
DB_PORT = os.environ.get('DB_PORT', '')
EMAIL_USERNAME = os.environ.get('EMAIL_USERNAME', '')
EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD', '')
TOKEN_DISCORD = os.environ.get('TOKEN_DISCORD', '') # Not implemented
TOKEN_SLACK = os.environ.get('TOKEN_SLACK', '')
TOKEN_TELEGRAM = os.environ.get('TOKEN_TELEGRAM', '')
DB_HOST = os.environ.get('DB_HOST', '')
DB_USERNAME = os.environ.get('DB_USERNAME', '')
DB_PASSWORD = os.environ.get('DB_PASSWORD', '')
DB_NAME = os.environ.get('DB_NAME', '')
DB_PORT = os.environ.get('DB_PORT', '')


## Pre-flight check
# We assert that the secrets are not empty so that
Expand Down
39 changes: 25 additions & 14 deletions node_monitor/node_monitor.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import time
from collections import deque
from typing import Deque, List, Dict
from typing import Deque, List, Dict, Optional
from toolz import groupby # type: ignore
import schedule

import node_monitor.ic_api as ic_api
from node_monitor.bot_email import EmailBot
from node_monitor.bot_slack import SlackBot
from node_monitor.bot_telegram import TelegramBot
from node_monitor.node_provider_db import NodeProviderDB
from node_monitor.node_monitor_helpers.get_compromised_nodes import \
get_compromised_nodes
Expand All @@ -19,20 +20,25 @@
class NodeMonitor:

def __init__(
self, email_bot: EmailBot, slack_bot: SlackBot,
node_provider_db: NodeProviderDB) -> None:
self,
node_provider_db: NodeProviderDB,
email_bot: EmailBot,
slack_bot: Optional[SlackBot] = None,
telegram_bot: Optional[TelegramBot] = None) -> None:
"""NodeMonitor is a class that monitors the status of the nodes.
It is responsible for syncing the nodes from the ic-api, analyzing
the nodes, and broadcasting alerts to the appropriate channels.
Args:
email_bot: An instance of EmailBot
slack_bot: An instance of SlackBot
telegram_bot: An instance of TelegramBot
node_provider_db: An instance of NodeProviderDB
Attributes:
email_bot: An instance of EmailBot
slack_bot: An instance of SlackBot
telegram_bot: An instance of TelegramBot
node_provider_db: An instance of NodeProviderDB
snapshots: A deque of the last 3 snapshots of the nodes
last_update: The timestamp of the last time the nodes were synced
Expand All @@ -43,9 +49,10 @@ def __init__(
node_provider_id, but only including node_providers that are
subscribed to alerts.
"""
self.node_provider_db = node_provider_db
self.email_bot = email_bot
self.slack_bot = slack_bot
self.node_provider_db = node_provider_db
self.telegram_bot = telegram_bot
self.snapshots: Deque[ic_api.Nodes] = deque(maxlen=3)
self.last_update: float | None = None
self.last_status_report: float = 0
Expand Down Expand Up @@ -106,15 +113,14 @@ def broadcast_alerts(self) -> None:
if preferences['notify_email'] == True:
recipients = email_recipients[node_provider_id]
self.email_bot.send_emails(recipients, subject, msg)
if preferences['notify_slack'] == True:
channel_name = channels[node_provider_id]['slack_channel_name']
self.slack_bot.send_message(channel_name, msg)
if preferences['notify_slack'] == True:
if self.slack_bot is not None:
channel_name = channels[node_provider_id]['slack_channel_name']
self.slack_bot.send_message(channel_name, msg)
if preferences['notify_telegram_chat'] == True:
# TODO: Not Yet Implemented
raise NotImplementedError
if preferences['notify_telegram_channel'] == True:
# TODO: Not Yet Implemented
raise NotImplementedError
if self.telegram_bot is not None:
chat_id = channels[node_provider_id]['telegram_chat_id']
self.telegram_bot.send_message(chat_id, msg)
# - - - - - - - - - - - - - - - - -


Expand Down Expand Up @@ -144,8 +150,13 @@ def broadcast_status_report(self) -> None:
recipients = email_recipients[node_provider_id]
self.email_bot.send_emails(recipients, subject, msg)
if preferences['notify_slack'] == True:
channel_name = channels[node_provider_id]['slack_channel_name']
self.slack_bot.send_message(channel_name, msg)
if self.slack_bot is not None:
channel_name = channels[node_provider_id]['slack_channel_name']
self.slack_bot.send_message(channel_name, msg)
if preferences['notify_telegram_chat'] == True:
if self.telegram_bot is not None:
chat_id = channels[node_provider_id]['telegram_chat_id']
self.telegram_bot.send_message(chat_id, msg)
# - - - - - - - - - - - - - - - - -


Expand Down
29 changes: 18 additions & 11 deletions node_monitor/node_provider_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ class NodeProviderDB:
notify_email BOOLEAN,
notify_slack BOOLEAN,
notify_telegram_chat BOOLEAN,
notify_telegram_channel BOOLEAN,
notify_telegram_channel BOOLEAN, --Deprecated: column not used
node_provider_name TEXT
);
"""
table_subscribers_cols = [
'node_provider_id', 'notify_on_status_change', 'notify_email',
'notify_slack', 'notify_telegram_chat', 'notify_telegram_channel',
'notify_slack', 'notify_telegram_chat',
'notify_telegram_channel', # Deprecated: column not used
'node_provider_name']


Expand All @@ -65,11 +66,13 @@ class NodeProviderDB:
node_provider_id TEXT,
slack_channel_name TEXT,
telegram_chat_id TEXT,
telegram_channel_id TEXT
telegram_channel_id TEXT --Deprecated: column not used
);
"""
table_channel_lookup_cols = ['id', 'node_provider_id', 'slack_channel_name',
'telegram_chat_id', 'telegram_channel_id']
'telegram_chat_id',
'telegram_channel_id' # Deprecated: column not used
]


# TABLE node_label_lookup
Expand Down Expand Up @@ -172,7 +175,8 @@ def get_public_schema_tables(self) -> List[str]:
def _insert_subscriber(
self, node_provider_id: Principal, notify_on_status_change: bool,
notify_email: bool, notify_slack: bool, notify_telegram_chat: bool,
notify_telegram_channel: bool, node_provider_name: str) -> None:
notify_telegram_channel: bool, # Deprecated: column not used
node_provider_name: str) -> None:
"""Inserts a subscriber into the subscribers table. Overwrites if
subscriber already exists."""
query = """
Expand All @@ -182,15 +186,15 @@ def _insert_subscriber(
notify_email,
notify_slack,
notify_telegram_chat,
notify_telegram_channel,
notify_telegram_channel, --Deprecated: colunn not used
node_provider_name
) VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (node_provider_id) DO UPDATE SET
notify_on_status_change = EXCLUDED.notify_on_status_change,
notify_email = EXCLUDED.notify_email,
notify_slack = EXCLUDED.notify_slack,
notify_telegram_chat = EXCLUDED.notify_telegram_chat,
notify_telegram_channel = EXCLUDED.notify_telegram_channel,
notify_telegram_channel = EXCLUDED.notify_telegram_channel, --Deprecated: column not used
node_provider_name = EXCLUDED.node_provider_name
"""
values = (
Expand All @@ -199,7 +203,7 @@ def _insert_subscriber(
notify_email,
notify_slack,
notify_telegram_chat,
notify_telegram_channel,
notify_telegram_channel, # Deprecated: column not used
node_provider_name
)
self.connect()
Expand Down Expand Up @@ -307,21 +311,24 @@ def get_emails_as_dict(self) -> Dict[Principal, List[str]]:

def _insert_channel(
self, node_provider_id: Principal, slack_channel_name: str,
telegram_chat_id: str, telegram_channel_id: str) -> None:
telegram_chat_id: str,
telegram_channel_id: str # Deprecated: column not used
) -> None:
"""Inserts or updates a record in the channel_lookup table."""
query = """
INSERT INTO channel_lookup (
node_provider_id,
slack_channel_name,
telegram_chat_id,
telegram_channel_id
telegram_channel_id --Deprecated: column not used
) VALUES (%s, %s, %s, %s)
"""
values = (
node_provider_id,
slack_channel_name,
telegram_chat_id,
telegram_channel_id)
telegram_channel_id # Deprecated: column not used
)
self.connect()
assert self.conn is not None
with self.conn.cursor() as cur:
Expand Down
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ def pytest_addoption(parser):
action="store_true",
default=False,
help="send actual slack messages via webclient to test slack channel")
parser.addoption(
"--send_telegram",
action="store_true",
default=False,
help="send actual telegram messages via http to test telegram channel")
parser.addoption(
"--db",
action="store_true",
Expand All @@ -35,6 +40,8 @@ def pytest_configure(config):
"markers", "live_email: test sends a live email over the network")
config.addinivalue_line(
"markers", "live_slack: test sends a live slack message over the network")
config.addinivalue_line(
"markers", "live_telegram: test sends a live telegram message over the network")
config.addinivalue_line(
"markers", "db: test CRUD operations on the database")

Expand All @@ -58,6 +65,11 @@ def pytest_collection_modifyitems(config, items):
for item in items:
if "live_slack" in item.keywords:
item.add_marker(skip_live_slack)
if not config.getoption("--send_telegram"):
skip_live_telegram = pytest.mark.skip(reason="need --send_telegram option to run")
for item in items:
if "live_telegram" in item.keywords:
item.add_marker(skip_live_telegram)



Expand Down
32 changes: 32 additions & 0 deletions tests/test_bot_telegram.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pytest
from unittest.mock import patch

import node_monitor.load_config as c
from node_monitor.bot_telegram import TelegramBot

@patch("requests.get")
def test_send_message(mock_get):
telegram_bot = TelegramBot(c.TOKEN_TELEGRAM)
chat_id = "1234567890"
message = "Test message"
mock_response = mock_get.return_value
mock_response.raise_for_status.return_value = None

telegram_bot.send_message(chat_id, message)

mock_get.assert_called_once_with(
f"https://api.telegram.org/bot{telegram_bot.telegram_token}/sendMessage?chat_id={chat_id}&text={message}"
)
mock_response.raise_for_status.assert_called_once()



@pytest.mark.live_telegram
def test_send_live_message():
telegram_bot = TelegramBot(c.TOKEN_TELEGRAM)
chat_id = "-1001925583150"
message = "Test message"

err = telegram_bot.send_message(chat_id, message)
if err is not None:
raise err
Loading

0 comments on commit 8751c7a

Please sign in to comment.