# What this PR does - Adds [`mypy` static type checking](https://mypy-lang.org/) to our CI pipeline. Currently there is still a **ton** of errors being returned by the tool, as we'll need to fix pre-existing errors. I think we can slowly chip away at these errors in small PRs, doing them all in one large PR is likely very risky. - Also, this PR starts chipping away at one of the main type errors that we have which is accessing the `datetime` class (from the `datetime` library) or `timedelta` function on the `django.utils.timezone` module. Basically we should be instead accessing these two objects from the native `datetime` module. This makes sense because the [`__all__` attribute](https://github.com/django/django/blob/main/django/utils/timezone.py#L14-L30) in `django.utils.timezone` does not re-export `datetime` or `timedelta`. - splits `engine` dependencies out into `requirements.txt` and `requirements-dev.txt` ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated (N/A) - [ ] Documentation added (or `pr:no public docs` PR label added if not required) (N/A) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) (N/A)
113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
import typing
|
|
from enum import Enum
|
|
|
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
|
|
|
from apps.alerts.models import AlertGroup
|
|
from apps.telegram.utils import CallbackQueryFactory
|
|
|
|
|
|
class Action(Enum):
|
|
ACKNOWLEDGE = "acknowledge"
|
|
UNACKNOWLEDGE = "unacknowledge"
|
|
RESOLVE = "resolve"
|
|
UNRESOLVE = "unresolve"
|
|
SILENCE = "silence"
|
|
UNSILENCE = "unsilence"
|
|
|
|
|
|
ACTION_TO_CODE_MAP = {
|
|
Action.ACKNOWLEDGE.value: 0,
|
|
Action.UNACKNOWLEDGE.value: 1,
|
|
Action.RESOLVE.value: 2,
|
|
Action.UNRESOLVE.value: 3,
|
|
Action.SILENCE.value: 4,
|
|
Action.UNSILENCE.value: 5,
|
|
}
|
|
|
|
CODE_TO_ACTION_MAP = {
|
|
0: Action.ACKNOWLEDGE.value,
|
|
1: Action.UNACKNOWLEDGE.value,
|
|
2: Action.RESOLVE.value,
|
|
3: Action.UNRESOLVE.value,
|
|
4: Action.SILENCE.value,
|
|
5: Action.UNSILENCE.value,
|
|
}
|
|
|
|
|
|
class TelegramKeyboardRenderer:
|
|
def __init__(self, alert_group: AlertGroup):
|
|
self.alert_group = alert_group
|
|
|
|
# Inline keyboard with controls for alert group message
|
|
def render_actions_keyboard(self) -> typing.Optional[InlineKeyboardMarkup]:
|
|
if self.alert_group.root_alert_group is not None:
|
|
# No keyboard for attached alert group
|
|
return None
|
|
|
|
rows = []
|
|
|
|
# Acknowledge/Unacknowledge button
|
|
if not self.alert_group.resolved:
|
|
rows.append([self.acknowledge_button])
|
|
|
|
# Resolve/Unresolve buttons
|
|
rows.append([self.resolve_button])
|
|
|
|
# Silence/Unsilence buttons
|
|
if not self.alert_group.resolved:
|
|
if not self.alert_group.silenced:
|
|
rows.append(self.silence_buttons)
|
|
else:
|
|
rows.append([self.unsilence_button])
|
|
|
|
return InlineKeyboardMarkup(rows)
|
|
|
|
@staticmethod
|
|
def render_link_to_channel_keyboard(link: str) -> InlineKeyboardMarkup:
|
|
button = InlineKeyboardButton(text="Go to the alert group", url=link)
|
|
return InlineKeyboardMarkup([[button]])
|
|
|
|
@property
|
|
def acknowledge_button(self) -> InlineKeyboardButton:
|
|
action = Action.ACKNOWLEDGE if not self.alert_group.acknowledged else Action.UNACKNOWLEDGE
|
|
return self._render_button(text=action.value.capitalize(), action=action)
|
|
|
|
@property
|
|
def resolve_button(self) -> InlineKeyboardButton:
|
|
action = Action.RESOLVE if not self.alert_group.resolved else Action.UNRESOLVE
|
|
return self._render_button(text=action.value.capitalize(), action=action)
|
|
|
|
@property
|
|
def silence_buttons(self) -> typing.List[InlineKeyboardButton]:
|
|
silence_forever_button = self._render_button(text="🔕 forever", action=Action.SILENCE)
|
|
|
|
silence_delay_one_hour = 3600 # one hour
|
|
silence_one_hour_button = self._render_button(
|
|
text="... for 1h", action=Action.SILENCE, action_data=silence_delay_one_hour
|
|
)
|
|
|
|
silence_delay_four_hours = 14400 # four hours
|
|
silence_four_hours_button = self._render_button(
|
|
text="... for 4h", action=Action.SILENCE, action_data=silence_delay_four_hours
|
|
)
|
|
|
|
return [silence_forever_button, silence_one_hour_button, silence_four_hours_button]
|
|
|
|
@property
|
|
def unsilence_button(self) -> InlineKeyboardButton:
|
|
return self._render_button(text=Action.UNSILENCE.value.capitalize(), action=Action.UNSILENCE)
|
|
|
|
def _render_button(self, text: str, action: Action, action_data: typing.Optional[typing.Union[int, str]] = None):
|
|
action_code = ACTION_TO_CODE_MAP[action.value]
|
|
callback_data_args: typing.List[typing.Union[int, str]] = [self.alert_group.pk, action_code]
|
|
if action_data is not None:
|
|
callback_data_args.append(action_data)
|
|
# Add org id with 'oncall' prefix to callback data.
|
|
# It's a workaround to pass oncall-uuid to the oncall-gateway while proxying requests.
|
|
# TODO: check if it's possible switch to json str instead of ':' separated string.
|
|
# Note, that there is a 64bytes limit to callback data
|
|
callback_data_args.append(f"oncall-uuid{self.alert_group.channel.organization.uuid}")
|
|
button = InlineKeyboardButton(text=text, callback_data=CallbackQueryFactory.encode_data(*callback_data_args))
|
|
|
|
return button
|