Skip to content

Commit

Permalink
feat: send alert image with telegram message, if requested
Browse files Browse the repository at this point in the history
- add boolean field send_message to recipient table
- add int field media_id to notification table
  • Loading branch information
Bruno Lenzi committed Aug 11, 2023
1 parent f4bce72 commit 01d5cfe
Show file tree
Hide file tree
Showing 11 changed files with 63 additions and 15 deletions.
8 changes: 7 additions & 1 deletion src/app/api/endpoints/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ async def alert_notification(payload: AlertOut):
}
subject: str = Template(recipient.subject_template).safe_substitute(**info)
message: str = Template(recipient.message_template).safe_substitute(**info)
notification = NotificationIn(alert_id=payload.id, recipient_id=recipient.id, subject=subject, message=message)
notification = NotificationIn(
alert_id=payload.id,
recipient_id=recipient.id,
subject=subject,
message=message,
media_id=payload.media_id if recipient.send_image else None,
)
await send_notification(notification)


Expand Down
1 change: 1 addition & 0 deletions src/app/models/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Media(Base):

device: RelationshipProperty = relationship("Device", uselist=False, back_populates="media")
alerts: RelationshipProperty = relationship("Alert", back_populates="media")
notifications: RelationshipProperty = relationship("Notification", back_populates="media")

def __repr__(self):
return f"<Media(device_id='{self.device_id}', bucket_key='{self.bucket_key}', type='{self.type}'>"
2 changes: 2 additions & 0 deletions src/app/models/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ class Notification(Base):
recipient_id = Column(Integer, ForeignKey("recipients.id"))
subject = Column(String, nullable=False)
message = Column(String, nullable=False)
media_id = Column(Integer, ForeignKey("media.id"))

alert: RelationshipProperty = relationship("Alert", back_populates="notifications")
recipient: RelationshipProperty = relationship("Recipient", back_populates="notifications")
media: RelationshipProperty = relationship("Media", back_populates="notifications")

def __repr__(self):
return (
Expand Down
3 changes: 2 additions & 1 deletion src/app/models/recipient.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import enum

from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String
from sqlalchemy import Boolean, Column, DateTime, Enum, ForeignKey, Integer, String
from sqlalchemy.orm import RelationshipProperty, relationship
from sqlalchemy.sql import func

Expand All @@ -28,6 +28,7 @@ class Recipient(Base):
address = Column(String, nullable=False)
subject_template = Column(String, nullable=False)
message_template = Column(String, nullable=False)
send_image = Column(Boolean)
created_at = Column(DateTime, default=func.now())

group: RelationshipProperty = relationship("Group", back_populates="recipients")
Expand Down
5 changes: 4 additions & 1 deletion src/app/schemas/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
# This program is licensed under the Apache License 2.0.
# See LICENSE or go to <https://opensource.org/licenses/Apache-2.0> for full license details.

from pydantic import BaseModel, Field
from typing import Optional

from pydantic import BaseModel, Field, PositiveInt

from app.schemas.base import _CreatedAt, _Id

Expand All @@ -13,6 +15,7 @@ class NotificationIn(BaseModel):
recipient_id: int = Field(description="linked recipient entry")
subject: str = Field(description="subject of notification")
message: str = Field(description="message of notification")
media_id: Optional[PositiveInt] = Field(description="id of media to be sent (or None)")


class NotificationOut(NotificationIn, _Id, _CreatedAt):
Expand Down
1 change: 1 addition & 0 deletions src/app/schemas/recipients.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class RecipientIn(_GroupId):
message_template: str = Field(
description="template for notification message. Can contain fields like $date that are replaced when the notification is sent"
)
send_image: bool = Field(description="send alert image together with notification message")


class RecipientOut(RecipientIn, _Id, _CreatedAt):
Expand Down
13 changes: 10 additions & 3 deletions src/app/services/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See LICENSE or go to <https://opensource.org/licenses/Apache-2.0> for full license details.

import logging
from typing import Optional
from typing import Optional, Union

import telegram

Expand All @@ -23,12 +23,15 @@ def resolve_bucket_key(file_name: str, bucket_folder: Optional[str] = None) -> s
return f"{bucket_folder}/{file_name}" if isinstance(bucket_folder, str) else file_name


async def send_telegram_msg(chat_id: str, text: str, test: bool = False) -> Optional[telegram.Message]:
async def send_telegram_msg(
chat_id: str, text: str, photo: Union[None, str, bytes] = None, test: bool = False
) -> Optional[telegram.Message]:
"""Send telegram message to the chat with the given id
Args:
chat_id (str): chat id
text (str): message to send
photo (str, bytes or None, default=None): photo to send
test (bool, default=False): disable notification and delete msg after sending. Used by unittests
Returns: response
Expand All @@ -38,7 +41,11 @@ async def send_telegram_msg(chat_id: str, text: str, test: bool = False) -> Opti
return None
async with telegram.Bot(cfg.TELEGRAM_TOKEN) as bot:
try:
msg: telegram.Message = await bot.send_message(text=text, chat_id=chat_id, disable_notification=test)
msg: telegram.Message = (
await bot.send_message(text=text, chat_id=chat_id, disable_notification=test)
if photo is None
else await bot.send_photo(chat_id=chat_id, photo=photo, caption=text)
)
except telegram.error.TelegramError as e:
logger.warning(f"Problem sending telegram message to {chat_id}: {e!s}")
else:
Expand Down
20 changes: 16 additions & 4 deletions src/tests/routes/test_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"address": "my_chat_id",
"subject_template": "New alert on $device_name",
"message_template": "Group 1: alert $alert_id issued by $device_name",
"send_image": True,
"created_at": "2020-10-13T08:18:45.447773",
},
{
Expand All @@ -164,6 +165,7 @@
"address": "my_other_chat_id",
"subject_template": "New alert on $device_name",
"message_template": "Group 2: alert $alert_id issued by $device_name",
"send_image": False,
"created_at": "2020-10-13T08:18:45.447773",
},
]
Expand All @@ -176,7 +178,7 @@
RECIPIENT_TABLE_FOR_DB = list(map(update_only_datetime, RECIPIENT_TABLE))


async def check_notifications(alert_id: int, device_id: int, is_new_event: bool):
async def check_notifications(alert_id: int, device_id: int, media_id: int, is_new_event: bool):
notifications = await crud.fetch_all(db.notifications, {"alert_id": alert_id})
assert len(notifications) == is_new_event
if not is_new_event:
Expand All @@ -186,12 +188,15 @@ async def check_notifications(alert_id: int, device_id: int, is_new_event: bool)
group_id = next(item["id"] for item in USER_TABLE if device["owner_id"] == item["id"])
login = device["login"]
break
recipient_id = next(item["id"] for item in RECIPIENT_TABLE if item["group_id"] == group_id)
recipient = next(item for item in RECIPIENT_TABLE if item["group_id"] == group_id)
recipient_id = recipient["id"]
media_id = media_id if recipient["send_image"] else None
expected_notification = {
"alert_id": alert_id,
"recipient_id": recipient_id,
"subject": f"New alert on {login}",
"message": f"Group {group_id}: alert {alert_id} issued by {login}",
"media_id": media_id,
}
assert {k: v for k, v in notifications[0].items() if k not in ("id", "created_at")} == expected_notification

Expand Down Expand Up @@ -367,7 +372,12 @@ async def test_create_alert(
new_alert = dict(**new_alert)
assert utc_dt < new_alert["created_at"] < datetime.utcnow()

await check_notifications(alert_id=new_alert["id"], device_id=payload["device_id"], is_new_event=is_new_event)
await check_notifications(
alert_id=new_alert["id"],
device_id=payload["device_id"],
media_id=new_alert["media_id"],
is_new_event=is_new_event,
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -431,7 +441,9 @@ async def test_create_alert_by_device(
new_alert = dict(**new_alert)
assert utc_dt < new_alert["created_at"] < datetime.utcnow()

await check_notifications(alert_id=new_alert["id"], device_id=device_id, is_new_event=is_new_event)
await check_notifications(
alert_id=new_alert["id"], device_id=device_id, media_id=new_alert["media_id"], is_new_event=is_new_event
)


@pytest.mark.parametrize(
Expand Down
2 changes: 2 additions & 0 deletions src/tests/routes/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"recipient_id": 1,
"subject": "New alert",
"message": "Alert issued",
"media_id": 1,
"created_at": "2020-10-13T08:18:45.447773",
},
{
Expand All @@ -33,6 +34,7 @@
"recipient_id": 2,
"subject": "New alert",
"message": "Alert issued",
"media_id": None,
"created_at": "2020-10-13T08:18:45.447773",
},
]
Expand Down
2 changes: 2 additions & 0 deletions src/tests/routes/test_recipients.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"address": "my_chat_id",
"subject_template": "New alert on $device",
"message_template": "Group 1: alert $alert_id issued by $device on $date",
"send_image": True,
"created_at": "2020-10-13T08:18:45.447773",
},
{
Expand All @@ -37,6 +38,7 @@
"address": "my_other_chat_id",
"subject_template": "New alert on $device",
"message_template": "Group 2: alert $alert_id issued by $device on $date",
"send_image": False,
"created_at": "2020-10-13T08:18:45.447773",
},
]
Expand Down
21 changes: 16 additions & 5 deletions src/tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,31 @@ async def test_no_send_telegram_msg(unset_telegram_token):
@pytest.mark.skipif(not cfg.TELEGRAM_TOKEN, reason="TELEGRAM_TOKEN not set")
@pytest.mark.asyncio
@pytest.mark.parametrize(
"chat_id, text, valid",
"chat_id, text, photo, valid",
[
("invalid-chat-id", "Fake message", False),
("invalid-chat-id", "Fake message", None, False),
pytest.param(
os.environ.get("TELEGRAM_TEST_CHAT_ID"),
"Test message",
None,
True,
marks=pytest.mark.skipif("TELEGRAM_TEST_CHAT_ID" not in os.environ, reason="TELEGRAM_TEST_CHAT_ID not set"),
),
pytest.param(
os.environ.get("TELEGRAM_TEST_CHAT_ID"),
"Test message with photo",
"https://avatars.githubusercontent.com/u/61667887?s=200&v=4",
True,
marks=pytest.mark.skipif("TELEGRAM_TEST_CHAT_ID" not in os.environ, reason="TELEGRAM_TEST_CHAT_ID not set"),
),
],
)
async def test_send_telegram_msg(chat_id, text, valid):
msg = await send_telegram_msg(chat_id=chat_id, text=text, test=True)
async def test_send_telegram_msg(chat_id, text, photo, valid):
msg = await send_telegram_msg(chat_id=chat_id, text=text, photo=photo, test=True)
if not valid:
assert msg is None
else:
elif photo is None:
assert msg.text == text
else:
assert msg.caption == text
assert len(msg.photo)

0 comments on commit 01d5cfe

Please sign in to comment.