This commit is contained in:
Joey Orlando 2024-08-19 14:08:40 -04:00 committed by GitHub
commit aa85994d78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 129 additions and 62 deletions

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
20.16.0

View file

@ -38,7 +38,7 @@ The following is required to connect to Microsoft Teams to Grafana OnCall:
- You must have Admin permissions in your Grafana Cloud instance.
- You must have Owner permissions in Microsoft Teams.
- Install the Grafana OnCall app from the [Microsoft Marketplace](https://appsource.microsoft.com/en-us/product/office/WA200004307).
- Install the Grafana IRM app from the [Microsoft Marketplace](https://appsource.microsoft.com/en-us/product/office/WA200004307).
## Install Microsoft Teams integration for Grafana OnCall

View file

@ -447,10 +447,9 @@ def test_pagination(settings, alert_group_public_api_setup):
token, alert_groups, _, _ = alert_group_public_api_setup
client = APIClient()
url = reverse("api-public:alert_groups-list")
url = "{}?perpage=1".format(reverse("api-public:alert_groups-list"))
with patch("common.api_helpers.paginators.PathPrefixedPagePagination.get_page_size", return_value=1):
response = client.get(url, HTTP_AUTHORIZATION=token)
response = client.get(url, HTTP_AUTHORIZATION=token)
assert response.status_code == status.HTTP_200_OK
result = response.json()

View file

@ -3,7 +3,7 @@ from typing import Optional, Tuple, Union
from django.conf import settings
from telegram import Bot, InlineKeyboardMarkup, Message, ParseMode
from telegram.error import BadRequest, InvalidToken, Unauthorized
from telegram.error import BadRequest, InvalidToken, TelegramError, Unauthorized
from telegram.utils.request import Request
from apps.alerts.models import AlertGroup
@ -27,6 +27,18 @@ class TelegramClient:
if self.token is None:
raise InvalidToken()
class BadRequestMessage:
CHAT_NOT_FOUND = "Chat not found"
MESSAGE_IS_NOT_MODIFIED = "Message is not modified"
MESSAGE_TO_EDIT_NOT_FOUND = "Message to edit not found"
NEED_ADMIN_RIGHTS_IN_THE_CHANNEL = "Need administrator rights in the channel chat"
MESSAGE_TO_BE_REPLIED_NOT_FOUND = "Message to be replied not found"
class UnauthorizedMessage:
BOT_WAS_BLOCKED_BY_USER = "Forbidden: bot was blocked by the user"
INVALID_TOKEN = "Invalid token"
USER_IS_DEACTIVATED = "Forbidden: user is deactivated"
@property
def api_client(self) -> Bot:
return Bot(self.token, request=Request(read_timeout=15))
@ -96,7 +108,7 @@ class TelegramClient:
disable_web_page_preview=False,
)
except BadRequest as e:
logger.warning("Telegram BadRequest: {}".format(e.message))
logger.warning(f"Telegram BadRequest: {e.message}")
raise
return message
@ -171,3 +183,7 @@ class TelegramClient:
raise Exception(f"_get_message_and_keyboard with type {message_type} is not implemented")
return text, keyboard
@staticmethod
def error_message_is(error: TelegramError, messages: list[str]) -> bool:
return error.message in messages

View file

@ -42,7 +42,7 @@ def ignore_message_unchanged(f):
try:
return f(*args, **kwargs)
except error.BadRequest as e:
if "Message is not modified" in e.message:
if TelegramClient.error_message_is(e, [TelegramClient.BadRequestMessage.MESSAGE_IS_NOT_MODIFIED]):
logger.warning(
f"Tried to change Telegram message, but update is identical to original message. "
f"args: {args}, kwargs: {kwargs}"
@ -59,7 +59,7 @@ def ignore_message_to_edit_deleted(f):
try:
return f(*args, **kwargs)
except error.BadRequest as e:
if "Message to edit not found" in e.message:
if TelegramClient.error_message_is(e, [TelegramClient.BadRequestMessage.MESSAGE_TO_EDIT_NOT_FOUND]):
logger.warning(
f"Tried to edit Telegram message, but message was deleted. args: {args}, kwargs: {kwargs}"
)
@ -75,7 +75,7 @@ def ignore_reply_to_message_deleted(f):
try:
return f(*args, **kwargs)
except error.BadRequest as e:
if "Replied message not found" in e.message:
if TelegramClient.error_message_is(e, [TelegramClient.BadRequestMessage.MESSAGE_TO_BE_REPLIED_NOT_FOUND]):
logger.warning(
f"Tried to reply to Telegram message, but message was deleted. args: {args}, kwargs: {kwargs}"
)

View file

@ -123,15 +123,16 @@ class TelegramToOrganizationConnector(models.Model):
chat_id=self.channel_chat_id, message_type=TelegramMessage.ALERT_GROUP_MESSAGE, alert_group=alert_group
)
except error.BadRequest as e:
if e.message == "Need administrator rights in the channel chat":
if TelegramClient.error_message_is(
e,
[
TelegramClient.BadRequestMessage.NEED_ADMIN_RIGHTS_IN_THE_CHANNEL,
TelegramClient.BadRequestMessage.CHAT_NOT_FOUND,
],
):
logger.warning(
f"Could not send alert group to Telegram channel with id {self.channel_chat_id} "
f"due to lack of admin rights. alert_group {alert_group.pk}"
)
elif e.message == "Chat not found":
logger.warning(
f"Could not send alert group to Telegram channel with id {self.channel_chat_id} "
f"due to 'Chat not found'. alert_group {alert_group.pk}"
f"due to {e.message}. alert_group {alert_group.pk}"
)
else:
telegram_client.send_message(

View file

@ -25,6 +25,14 @@ class TelegramToUserConnector(models.Model):
class Meta:
unique_together = (("user", "telegram_chat_id"),)
class NotificationErrorReason:
USER_WAS_DISABLED = "Telegram user was disabled"
INVALID_TOKEN = "Invalid token"
BOT_BLOCKED_BY_USER = "Bot was blocked by the user"
FORMATTING_ERROR_IN_RENDERED_TEMPLATE = (
"Notification sent but there was a formatting error in the rendered template"
)
@classmethod
def notify_user(cls, user: User, alert_group: AlertGroup, notification_policy: UserNotificationPolicy) -> None:
try:
@ -98,7 +106,7 @@ class TelegramToUserConnector(models.Model):
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR,
reason="Invalid token",
reason=self.NotificationErrorReason.INVALID_TOKEN,
)
return
@ -123,7 +131,7 @@ class TelegramToUserConnector(models.Model):
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORMATTING_ERROR,
reason="Notification sent but there was a formatting error in the rendered template",
reason=self.NotificationErrorReason.FORMATTING_ERROR_IN_RENDERED_TEMPLATE,
)
telegram_client.send_message(
chat_id=self.telegram_chat_id,
@ -131,29 +139,29 @@ class TelegramToUserConnector(models.Model):
alert_group=alert_group,
)
except error.Unauthorized as e:
if e.message == "Forbidden: bot was blocked by the user":
if TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.BOT_WAS_BLOCKED_BY_USER]):
TelegramToUserConnector.create_telegram_notification_error(
alert_group,
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_BOT_IS_DELETED,
reason="Bot was blocked by the user",
reason=self.NotificationErrorReason.BOT_BLOCKED_BY_USER,
)
elif e.message == "Invalid token":
elif TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.INVALID_TOKEN]):
TelegramToUserConnector.create_telegram_notification_error(
alert_group,
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR,
reason="Invalid token",
reason=self.NotificationErrorReason.INVALID_TOKEN,
)
elif e.message == "Forbidden: user is deactivated":
elif TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.USER_IS_DEACTIVATED]):
TelegramToUserConnector.create_telegram_notification_error(
alert_group,
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED,
reason="Telegram user was disabled",
reason=self.NotificationErrorReason.USER_WAS_DISABLED,
)
else:
raise e
@ -175,7 +183,7 @@ class TelegramToUserConnector(models.Model):
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR,
reason="Invalid token",
reason=self.NotificationErrorReason.INVALID_TOKEN,
)
return
@ -193,29 +201,29 @@ class TelegramToUserConnector(models.Model):
alert_group=alert_group,
)
except error.Unauthorized as e:
if e.message == "Forbidden: bot was blocked by the user":
if TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.BOT_WAS_BLOCKED_BY_USER]):
TelegramToUserConnector.create_telegram_notification_error(
alert_group,
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_BOT_IS_DELETED,
reason="Bot was blocked by the user",
reason=self.NotificationErrorReason.BOT_BLOCKED_BY_USER,
)
elif e.message == "Invalid token":
elif TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.INVALID_TOKEN]):
TelegramToUserConnector.create_telegram_notification_error(
alert_group,
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR,
reason="Invalid token",
reason=self.NotificationErrorReason.INVALID_TOKEN,
)
elif e.message == "Forbidden: user is deactivated":
elif TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.USER_IS_DEACTIVATED]):
TelegramToUserConnector.create_telegram_notification_error(
alert_group,
self.user,
notification_policy,
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED,
reason="Telegram user was disabled",
reason=self.NotificationErrorReason.USER_WAS_DISABLED,
)
else:
raise e

View file

@ -66,7 +66,7 @@ def edit_message(self, message_pk):
try:
telegram_client.edit_message(message=message)
except error.BadRequest as e:
if "Message is not modified" in e.message:
if TelegramClient.error_message_is(e, [TelegramClient.BadRequestMessage.MESSAGE_IS_NOT_MODIFIED]):
pass
except (error.RetryAfter, error.TimedOut) as e:
countdown = getattr(e, "retry_after", 3)
@ -165,20 +165,19 @@ def send_log_and_actions_message(self, channel_chat_id, group_chat_id, channel_m
reply_to_message_id=reply_to_message_id,
)
except error.BadRequest as e:
if e.message == "Chat not found":
if TelegramClient.error_message_is(
e,
[
TelegramClient.BadRequestMessage.CHAT_NOT_FOUND,
TelegramClient.BadRequestMessage.MESSAGE_TO_BE_REPLIED_NOT_FOUND,
],
):
logger.warning(
f"Could not send log and actions messages to Telegram group with id {group_chat_id} "
f"due to 'Chat not found'. alert_group {alert_group.pk}"
f"due to '{e.message}'. alert_group {alert_group.pk}"
)
return
elif e.message == "Message to reply not found":
logger.warning(
f"Could not send log and actions messages to Telegram group with id {group_chat_id} "
f"due to 'Message to reply not found'. alert_group {alert_group.pk}"
)
return
else:
raise
raise
@shared_dedicated_queue_retry_task(

View file

@ -8,7 +8,11 @@ from apps.telegram.client import TelegramClient
from apps.telegram.models import TelegramMessage, TelegramToUserConnector
@patch.object(TelegramClient, "send_raw_message", side_effect=error.BadRequest("Replied message not found"))
@patch.object(
TelegramClient,
"send_raw_message",
side_effect=error.BadRequest(TelegramClient.BadRequestMessage.MESSAGE_TO_BE_REPLIED_NOT_FOUND),
)
@pytest.mark.django_db
def test_personal_connector_replied_message_not_found(
mock_send_message,
@ -53,19 +57,19 @@ def test_personal_connector_replied_message_not_found(
"side_effect,notification_error_code,reason",
[
(
error.Unauthorized("Forbidden: bot was blocked by the user"),
error.Unauthorized(TelegramClient.UnauthorizedMessage.BOT_WAS_BLOCKED_BY_USER),
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_BOT_IS_DELETED,
"Bot was blocked by the user",
TelegramToUserConnector.NotificationErrorReason.BOT_BLOCKED_BY_USER,
),
(
error.Unauthorized("Invalid token"),
error.Unauthorized(TelegramClient.UnauthorizedMessage.INVALID_TOKEN),
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR,
"Invalid token",
TelegramToUserConnector.NotificationErrorReason.INVALID_TOKEN,
),
(
error.Unauthorized("Forbidden: user is deactivated"),
error.Unauthorized(TelegramClient.UnauthorizedMessage.USER_IS_DEACTIVATED),
UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED,
"Telegram user was disabled",
TelegramToUserConnector.NotificationErrorReason.USER_WAS_DISABLED,
),
],
)

View file

@ -7,8 +7,10 @@ from apps.telegram.client import TelegramClient
from apps.telegram.models import TelegramMessage
from apps.telegram.tasks import send_log_and_actions_message
bad_request_error_msg = TelegramClient.BadRequestMessage.MESSAGE_TO_BE_REPLIED_NOT_FOUND
@patch.object(TelegramClient, "send_raw_message", side_effect=error.BadRequest("Message to reply not found"))
@patch.object(TelegramClient, "send_raw_message", side_effect=error.BadRequest(bad_request_error_msg))
@pytest.mark.django_db
def test_send_log_and_actions_replied_message_not_found(
mock_send_message,
@ -42,6 +44,6 @@ def test_send_log_and_actions_replied_message_not_found(
expected_msg = (
f"Could not send log and actions messages to Telegram group with id group_chat_id "
f"due to 'Message to reply not found'. alert_group {alert_group.pk}"
f"due to '{bad_request_error_msg}'. alert_group {alert_group.pk}"
)
assert expected_msg in caplog.text

View file

@ -1,5 +1,6 @@
import typing
from django.core.paginator import EmptyPage
from rest_framework.pagination import BasePagination, CursorPagination, PageNumberPagination
from rest_framework.response import Response
@ -54,6 +55,44 @@ class PathPrefixedPagePagination(BasePathPrefixedPagination, PageNumberPaginatio
)
return paginated_schema
def paginate_queryset(self, queryset, request, view=None):
request.build_absolute_uri = lambda: create_engine_url(request.get_full_path())
per_page = request.query_params.get(self.page_size_query_param, self.page_size)
try:
per_page = int(per_page)
except ValueError:
per_page = self.page_size
if per_page < 1:
per_page = self.page_size
paginator = self.django_paginator_class(queryset, per_page)
page_number = request.query_params.get(self.page_query_param, 1)
try:
page_number = int(page_number)
except ValueError:
page_number = 1
if page_number < 1:
page_number = 1
try:
self.page = self.get_page(page_number, paginator)
except EmptyPage:
self.page = paginator.page(paginator.num_pages)
if paginator.num_pages > 1 and self.template is not None:
self.display_page_controls = True
self.request = request
return list(self.page)
def get_page(self, page_number, paginator):
try:
return paginator.page(page_number)
except EmptyPage:
return paginator.page(paginator.num_pages)
class PathPrefixedCursorPagination(BasePathPrefixedPagination, CursorPagination):
def get_paginated_response(self, data: PaginatedData) -> Response:

View file

@ -1 +0,0 @@
20.x

View file

@ -126,9 +126,6 @@
"webpack-cli": "^5.1.4",
"webpack-livereload-plugin": "^3.0.2"
},
"engines": {
"node": "20.x"
},
"dependencies": {
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^7.0.0",

View file

@ -79,20 +79,20 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
<a href="https://appsource.microsoft.com/en-us/product/office/WA200004307" target="_blank" rel="noreferrer">
<Text type="link">MS Teams marketplace</Text>
</a>{' '}
and add <Text type="primary">Grafana OnCall app</Text> to your MS Teams org workspace.{' '}
and add <Text type="primary">Grafana IRM app</Text> to your MS Teams org workspace.{' '}
</Text>
)}
<Text type="secondary">
{!onCallisAdded ? 2 : 1}.{' '}
{personalSettings ? (
<Text type="secondary">
Send a direct message to the Grafana OnCall bot using <Text type="primary">linkUser</Text> command with
Send a direct message to the Grafana IRM bot using <Text type="primary">linkUser</Text> command with
following code:
</Text>
) : (
<Text type="secondary">
Add OnCall bot to your team channel and send this code by{' '}
<Text type="primary">@Grafana OnCall linkTeam</Text> command
Add IRM bot to your team channel and send this code by <Text type="primary">@Grafana IRM linkTeam</Text>{' '}
command
</Text>
)}
<Field className={cx('field-command')}>

View file

@ -660,6 +660,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
invalidateRequestFn = (requestedPage: number) => {
const { store } = this.props;
return requestedPage !== store.filtersStore.currentTablePageNum[PAGE.Integrations];
};
@ -696,6 +697,10 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
const { alertReceiveChannelStore } = store;
const newPage = isOnMount ? store.filtersStore.currentTablePageNum[PAGE.Integrations] : 1;
runInAction(() => {
store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage;
});
await alertReceiveChannelStore.fetchPaginatedItems({
filters: this.getFiltersBasedOnCurrentTab(),
page: newPage,
@ -703,9 +708,6 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
invalidateFn: () => this.invalidateRequestFn(newPage),
});
runInAction(() => {
store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage;
});
LocationHelper.update({ p: newPage }, 'partial');
};