Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(slack): invite specific Slack user groups for P1 incidents #110

Merged
merged 5 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions docs/usage/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,34 @@ Some tags have special meaning:
- `dev_firefighter`: Where users can get help with the bot. Will be shown in `/incident help` for instance.
- `it_deploy`: Where the bot send notifications for deployment freezes.

##### Usergroups
## User Group Management in Back-Office

You can add or import usergroups in the back-office.
You can **add** or **import user groups** in the back-office.

!!! note "Hint"
When adding a usergroup in the BackOffice, you can put only its ID. The rest of the information will be fetched from Slack.

### How users are invited into an incident

Users are invited to incidents through a system that listens for invitation requests. For critical incidents, specific user groups are automatically included in the invitation process.

The system also checks if the incident is public or private, ensuring that only the appropriate users with Slack accounts are invited. This creates a complete list of responders from all connected platforms, making sure the right people are notified.

### Custom Invitation Strategy

For users looking to create a custom invitation strategy, here’s what you need to know:

- **Django Signals**: We use Django signals to manage invitations. You can refer to the [Django signals documentation](https://docs.djangoproject.com/en/4.2/topics/signals/) for more information.


- **Registering on the Signal**: You need to register on the [`get_invites`][firefighter.incidents.signals.get_invites] signal, which provides the incident object and expects to receive a list of [`users`][firefighter.slack.models.user].

- **Signal Example**: You can check one of our [signals][firefighter.slack.signals.get_users] for a concrete example.

!!! note "Tips"
The signal can be triggered during the creation and update of an incident.
Invitations will only be sent once all signals have responded. It is advisable to avoid API calls and to store data in the database beforehand.

##### SOSes

You can configure [SOSes][firefighter.slack.models.sos.Sos] in the back-office.
Expand Down
1 change: 1 addition & 0 deletions src/firefighter/incidents/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@


class Group(models.Model):
"""Group of [Components][firefighter.incidents.models.component.Component]. Not a group of users."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=128, unique=True)
description = models.TextField(blank=True)
Expand Down
4 changes: 2 additions & 2 deletions src/firefighter/incidents/models/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
logger = logging.getLogger(__name__)

if TYPE_CHECKING:
from collections.abc import Sequence # noqa: F401
from collections.abc import Iterable, Sequence # noqa: F401
from decimal import Decimal
from uuid import UUID

Expand Down Expand Up @@ -460,7 +460,7 @@ def build_invite_list(self) -> list[User]:
users_list: list[User] = []

# Send signal to modules (Confluence, PagerDuty...)
result_users: list[tuple[Any, Exception | list[User]]] = (
result_users: list[tuple[Any, Exception | Iterable[User]]] = (
signals.get_invites.send_robust(sender=None, incident=self)
)

Expand Down
39 changes: 38 additions & 1 deletion src/firefighter/slack/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,45 @@ class MessageAdmin(admin.ModelAdmin[Message]):
class UserGroupAdmin(admin.ModelAdmin[UserGroup]):
model = UserGroup

list_display = [
"name",
"handle",
"usergroup_id",
"is_external",
"tag",
]

list_display_links = [
"name",
"handle",
"usergroup_id",
]

readonly_fields = (
"created_at",
"updated_at",
)

autocomplete_fields = ["components", "members"]
search_fields = ["name", "handle", "usergroup_id"]
search_fields = ["name", "handle", "description", "usergroup_id", "tag"]

fieldsets = (
(
("Slack attributes"),
{
"description" : ("These fields are synchronized automatically with Slack API"),
"fields": (
"name",
"handle",
"usergroup_id",
"description",
"is_external",
"members",
)
},
),
(_("Firefighter attributes"), {"fields": ("tag", "components", "created_at", "updated_at")}),
)

def save_model(
self,
Expand Down
22 changes: 22 additions & 0 deletions src/firefighter/slack/migrations/0002_usergroup_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.16 on 2024-10-14 10:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("slack", "0001_initial_oss"),
]

operations = [
migrations.AddField(
model_name="usergroup",
name="tag",
field=models.CharField(
blank=True,
help_text="Used by FireFighter internally to mark special users group (@team-secu, @team-incidents ...). Must be empty or unique.",
max_length=80,
),
),
]
6 changes: 6 additions & 0 deletions src/firefighter/slack/models/user_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ class UserGroup(models.Model):
help_text="Incident created with this usergroup automatically add the group members to these components.",
)

tag = models.CharField(
max_length=80,
blank=True,
help_text="Used by FireFighter internally to mark special user groups (e.g. @team-secu, @team-incidents...). Must be empty or unique.",
)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

Expand Down
32 changes: 29 additions & 3 deletions src/firefighter/slack/signals/get_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,22 @@

from firefighter.incidents import signals
from firefighter.incidents.models.user import User
from firefighter.slack.models.user_group import UserGroup
from firefighter.slack.slack_app import SlackApp

if TYPE_CHECKING:
from collections.abc import Iterable

from django.db.models.query import QuerySet

from firefighter.incidents.models.incident import Incident
from firefighter.slack.models.conversation import Conversation
from firefighter.slack.models.user_group import UserGroup

logger = logging.getLogger(__name__)


@receiver(signal=signals.get_invites)
def get_invites_from_slack(incident: Incident, **_kwargs: Any) -> list[User]:
def get_invites_from_slack(incident: Incident, **_kwargs: Any) -> Iterable[User]:
"""New version using cached users instead of querying Slack API."""
# Prepare sub-queries
slack_usergroups: QuerySet[UserGroup] = incident.component.usergroups.all()
Expand All @@ -39,4 +41,28 @@ def get_invites_from_slack(incident: Incident, **_kwargs: Any) -> list[User]:
)
.distinct()
)
return list(queryset)
return set(queryset)


@receiver(signal=signals.get_invites)
def get_invites_from_slack_for_p1(incident: Incident, **kwargs: Any) -> Iterable[User]:

if incident.priority.value > 1:
return []

if incident.private:
return []

slack_usergroups: QuerySet[UserGroup] = UserGroup.objects.filter(
tag="invited_for_all_public_p1"
)

queryset = (
User.objects.filter(slack_user__isnull=False)
.exclude(slack_user__slack_id=SlackApp().details["user_id"])
.exclude(slack_user__slack_id="")
.exclude(slack_user__slack_id__isnull=True)
.filter(usergroup__in=slack_usergroups)
.distinct()
)
return set(queryset)