Skip to content
2 changes: 2 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ DJANGO_SLACK_CLIENT_ID=
DJANGO_SLACK_CLIENT_SECRET=
DJANGO_SLACK_SEARCH_TOKEN=
DJANGO_SLACK_SIGNING_SECRET=
# Replace T1234567890 with the actual Slack workspace ID.
SLACK_BOT_TOKEN_T1234567890=
GITHUB_TOKEN=your-github-token

# Postgres image reads these on first volume init (must match DJANGO_DB_* above)
Expand Down
2 changes: 2 additions & 0 deletions backend/src/apps/slack/MANIFEST.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ oauth_config:
- im:write
- mpim:read
- mpim:write
- reactions:read
- reactions:write
- users:read
settings:
Expand All @@ -134,6 +135,7 @@ settings:
- app_mention
- member_joined_channel
- message.channels
- reaction_added
- team_join
interactivity:
is_enabled: true
Expand Down
2 changes: 2 additions & 0 deletions backend/src/apps/slack/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
from .event import EventAdmin
from .member import MemberAdmin
from .message import MessageAdmin
from .reaction_alert import ReactionAlertAdmin
from .reaction_rule import ReactionRuleAdmin
from .workspace import WorkspaceAdmin
34 changes: 34 additions & 0 deletions backend/src/apps/slack/admin/reaction_alert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Django admin screen for emitted Slack reaction alerts."""

from django.contrib import admin

from apps.slack.models.reaction_alert import ReactionAlert


@admin.register(ReactionAlert)
class ReactionAlertAdmin(admin.ModelAdmin):
"""Admin list/search controls for emitted reaction alerts."""

list_display = (
"conversation",
"message_ts",
"report_type",
"reaction_count",
"nest_created_at",
)
search_fields = ("conversation__name", "message_ts", "report_type")
readonly_fields = (
"conversation",
"message_ts",
"report_type",
"reaction_count",
"alert_message_ts",
)

def has_add_permission(self, request):
"""Disable manual alert creation in Django admin."""
return False

def has_delete_permission(self, request, obj=None):
"""Disable manual alert deletion in Django admin."""
return False
14 changes: 14 additions & 0 deletions backend/src/apps/slack/admin/reaction_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Django admin screen for Slack reaction rules."""

from django.contrib import admin

from apps.slack.models.reaction_rule import ReactionRule


@admin.register(ReactionRule)
class ReactionRuleAdmin(admin.ModelAdmin):
"""Admin list/search controls for reaction rules."""

list_display = ("conversation", "emoji_name", "report_type", "threshold", "is_enabled")
list_filter = ("is_enabled", "report_type")
search_fields = ("conversation__name", "emoji_name", "alert_channel_id")
1 change: 1 addition & 0 deletions backend/src/apps/slack/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ def configure_slack_events():
app_home_opened,
app_mention,
message_posted,
reaction_added,
team_join,
url_verification,
)
Expand Down
14 changes: 14 additions & 0 deletions backend/src/apps/slack/events/reaction_added.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Handle Slack reaction_added events."""

from apps.slack.events.event import EventBase
from apps.slack.services.reaction import process_reaction_added


class ReactionAdded(EventBase):
"""Route reaction_added to the reaction service for processing."""

event_type = "reaction_added"

def handle_event(self, event, client):
"""Handle report reactions added to Slack messages."""
process_reaction_added(event, client)
6 changes: 3 additions & 3 deletions backend/src/apps/slack/events/url_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ class UrlVerification(EventBase):

event_type = "url_verification"

def handle_event(self, event, client):
"""Handle the URL verification event."""
return event["challenge"]
def handler(self, event, client, ack):
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"""Acknowledge Slack URL verification challenges."""
ack(event["challenge"])
Comment thread
coderabbitai[bot] marked this conversation as resolved.
113 changes: 113 additions & 0 deletions backend/src/apps/slack/migrations/0023_reactionalert_reactionrule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Generated by Django 6.0.6 on 2026-06-22 20:33

import django.core.validators
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("slack", "0022_workspace_invite_link_last_alert_message_ts_and_more"),
]

operations = [
migrations.CreateModel(
name="ReactionAlert",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("nest_created_at", models.DateTimeField(auto_now_add=True)),
("nest_updated_at", models.DateTimeField(auto_now=True)),
(
"message_ts",
models.CharField(
help_text="Slack timestamp of the message that triggered the alert.",
max_length=32,
),
),
(
"report_type",
models.CharField(
help_text="Report category for the emitted reaction alert.",
max_length=64,
),
),
("reaction_count", models.PositiveSmallIntegerField(default=0)),
(
"alert_message_ts",
models.CharField(
blank=True,
default="",
help_text="Slack timestamp of the posted reaction alert message.",
max_length=32,
),
),
(
"conversation",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="slack.conversation"
),
),
],
options={
"db_table": "slack_reaction_alerts",
"unique_together": {("conversation", "message_ts", "report_type")},
},
),
migrations.CreateModel(
name="ReactionRule",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("nest_created_at", models.DateTimeField(auto_now_add=True)),
("nest_updated_at", models.DateTimeField(auto_now=True)),
(
"emoji_name",
models.CharField(
help_text="Slack emoji name that triggers this reaction rule.",
max_length=64,
),
),
(
"report_type",
models.CharField(
help_text="Report category recorded when this reaction rule triggers.",
max_length=64,
),
),
(
"threshold",
models.PositiveSmallIntegerField(
default=3, validators=[django.core.validators.MinValueValidator(1)]
),
),
(
"alert_channel_id",
models.CharField(
help_text="Slack channel ID where reaction alerts are posted.",
max_length=50,
),
),
("alert_user_ids", models.JSONField(blank=True, default=list)),
("is_enabled", models.BooleanField(default=True)),
(
"conversation",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="slack.conversation"
),
),
],
options={
"db_table": "slack_reaction_rules",
"unique_together": {("conversation", "emoji_name")},
},
),
]
2 changes: 2 additions & 0 deletions backend/src/apps/slack/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
from .event import Event
from .member import Member
from .message import Message
from .reaction_alert import ReactionAlert
from .reaction_rule import ReactionRule
from .workspace import Workspace
33 changes: 33 additions & 0 deletions backend/src/apps/slack/models/reaction_alert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Store emitted Slack reaction alerts."""

from django.db import models

from apps.common.models import TimestampedModel
from apps.slack.models.conversation import Conversation


class ReactionAlert(TimestampedModel):
"""Record that an alert was already sent for a reported message."""

class Meta:
"""Model options."""

db_table = "slack_reaction_alerts"
unique_together = ("conversation", "message_ts", "report_type")

conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE)
message_ts = models.CharField(
max_length=32,
help_text="Slack timestamp of the message that triggered the alert.",
)
report_type = models.CharField(
max_length=64,
help_text="Report category for the emitted reaction alert.",
)
reaction_count = models.PositiveSmallIntegerField(default=0)
alert_message_ts = models.CharField(
max_length=32,
blank=True,
default="",
help_text="Slack timestamp of the posted reaction alert message.",
)
40 changes: 40 additions & 0 deletions backend/src/apps/slack/models/reaction_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Store channel-specific Slack reaction rules."""

from typing import override

from django.core.validators import MinValueValidator
from django.db import models

from apps.common.models import TimestampedModel
from apps.slack.models.conversation import Conversation


class ReactionRule(TimestampedModel):
"""Channel-specific emoji threshold and alert target."""

class Meta:
"""Model options."""

db_table = "slack_reaction_rules"
unique_together = ("conversation", "emoji_name")

conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE)
emoji_name = models.CharField(
max_length=64,
help_text="Slack emoji name that triggers this reaction rule.",
)
report_type = models.CharField(
max_length=64,
help_text="Report category recorded when this reaction rule triggers.",
)
threshold = models.PositiveSmallIntegerField(default=3, validators=[MinValueValidator(1)])
alert_channel_id = models.CharField(
max_length=50,
help_text="Slack channel ID where reaction alerts are posted.",
)
alert_user_ids = models.JSONField(blank=True, default=list)
is_enabled = models.BooleanField(default=True)

@override
def __str__(self):
return f"{self.conversation} :{self.emoji_name}"
Loading
Loading