Repository navigation
feat(slack): fire TaskSpawners on emoji reactions - #1807
knechtionscoding wants to merge 1 commit into
Conversation
ab124ac to
ea4404f
Compare
ea4404f to
251b78f
Compare
251b78f to
54c788e
Compare
There was a problem hiding this comment.
All reported issues were addressed across 17 files
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
54c788e to
8e94d06
Compare
|
bump @gjkim |
1 similar comment
|
bump @gjkim |
Add spec.when.slack.triggers[].reaction, a block whose name is a canonical Slack emoji name. A reaction trigger fires when someone adds that emoji to a message: the Slack server fetches the message (channel history, or the thread for a reply) and creates a Task for it, honoring channels, excludeFilters and excludePatterns. A reaction trigger cannot set pattern or mentionOptional, and a pattern trigger never fires on a reaction. Nesting the emoji under reaction leaves room for per-reaction options without growing the CEL rule. Empty triggers still mean every bot mention fires. A spawner whose triggers are all reaction triggers fires on reactions only, so bot mentions and slash commands do not fire it; adding an empty trigger restores firing on every bot mention. Reactions by the bot itself and reactions on non-message items are ignored; reactions by other bots fire. Before fetching the message, the handler checks the emoji, channels and excludeFilters, so reactions no spawner wants cost no Slack API calls. Task names hash the channel, message ts and emoji, so the same emoji added by a second person resolves to the existing Task. The task gets new template variables ChannelID, MessageTS, ThreadTS, Reaction and ReactionUserID (present and empty for message-triggered Tasks), Kind "SlackReaction", and the reactor as kelos.dev/slack-user-id. kelos get taskspawner shows reaction triggers as :emoji: in the triggers list. The reaction field exists only in v1alpha2. On conversion to v1alpha1, reaction triggers are removed from the triggers list, where they would read as empty triggers that fire on every bot mention, and preserved in a kelos.dev/v1alpha2-slack-reaction-triggers annotation that is re-validated against the field's markers on restore. A reaction on a bot message with section or context blocks routes on those blocks' text, and fills Body with it. Slack can return a bot reply's text field as one line with its newlines collapsed, which a line-anchored excludePattern never matches. A person's message routes on its text, since its rich_text blocks drop mentions. The Slack app needs the reaction_added event and the reactions:read scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8e94d06 to
bee6a54
Compare
There was a problem hiding this comment.
1 issue found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="api/v1alpha2/taskspawner_types.go">
<violation number="1" location="api/v1alpha2/taskspawner_types.go:766">
P3: This comment overstates block-text handling: normal bot message events still match `ExcludePatterns` against `event.Text`; only reaction fetches call `reactedText` to extract section/context blocks. Qualify this clause as applying to reactions on bot messages, or update the normal message path to match the documented behavior.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| // matching so patterns target semantic content. Does NOT apply to | ||
| // slash commands. | ||
| // matching so patterns target semantic content. For a reaction trigger, | ||
| // the text is the reacted-to message's; for a bot message with section or |
There was a problem hiding this comment.
P3: This comment overstates block-text handling: normal bot message events still match ExcludePatterns against event.Text; only reaction fetches call reactedText to extract section/context blocks. Qualify this clause as applying to reactions on bot messages, or update the normal message path to match the documented behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At api/v1alpha2/taskspawner_types.go, line 766:
<comment>This comment overstates block-text handling: normal bot message events still match `ExcludePatterns` against `event.Text`; only reaction fetches call `reactedText` to extract section/context blocks. Qualify this clause as applying to reactions on bot messages, or update the normal message path to match the documented behavior.</comment>
<file context>
@@ -763,7 +763,8 @@ type Slack struct {
// syntax (RE2, unanchored). Leading @-mentions are stripped before
// matching so patterns target semantic content. For a reaction trigger,
- // the text is the reacted-to message's. Does NOT apply to slash commands.
+ // the text is the reacted-to message's; for a bot message with section or
+ // context blocks, it is those blocks' text. Does NOT apply to slash commands.
// +optional
</file context>
| // the text is the reacted-to message's; for a bot message with section or | |
| // the text is the reacted-to message's; for a reaction on a bot message with section or |
|
@gjkim42 bump here |
What type of PR is this?
/kind api
What this PR does / why we need it:
Adds a reaction trigger to Slack TaskSpawners
(
spec.when.slack.triggers[].reaction), so a spawner can fire when someone addsan emoji reaction to a message. Today a Slack spawner only fires on messages (a
bot mention or a trigger pattern) and on slash commands. A reaction lets a
person act on a message with one click. For example, ✅ on a proposal an agent
posted approves it, and ⚙️ on an announcement records it.
Matching.
pattern trigger never fires on a reaction.
reaction.nameis the emoji's canonical Slack name:+1, not the aliasthumbsup, because Slack reports reactions by canonical name. A skin-tonevariant such as
+1::skin-tone-2matches its base name.patternormentionOptional, which a CELrule on
SlackTriggerenforces.channelsandexcludeFiltersapply as they do to messages.excludePatternsis matched against the reacted-to message's text. For a botmessage with section or context blocks it is matched against those blocks'
text, which also fills
{{.Body}}: Slack can return a bot reply's text fieldas one line with its newlines collapsed, which a line-anchored pattern never
matches. A person's message keeps its text, since its
rich_textblocks dropmentions.
botMessagePolicydoes not apply. Reactions added by other bots fire thetrigger; only reactions the bot adds itself are ignored, as are reactions on
anything other than a message.
Which events fire the spawner.
triggers: every bot mention fires, as before.and slash commands do not fire it.
on matching messages, and on slash commands again. The
Triggersgodoc andthe
triggers[].patternreference row both say so.{}): the spawner fires onreactions and on every bot mention.
Handling.
reaction_addedevent carries only the channel and timestamp, so theSlack server reads the message before matching. It reads from
conversations.history, and for a thread reply fromconversations.replies.channelsandexcludeFilters, so areaction no spawner wants costs no Slack API calls.
kelos.dev/slack-user-idannotation names the person who reacted.Dedup. The Task name hashes the channel, message timestamp and emoji. When
a second person adds the same emoji, it resolves to the existing Task. A
different emoji on the same message gets its own Task.
Template variables.
{{.ChannelID}},{{.MessageTS}},{{.ThreadTS}},{{.Reaction}}and{{.ReactionUserID}}.Tasks too. That way a shared template renders under
missingkey=error.{{.Kind}}isSlackReactionfor these Tasks.{{.MessageTS}}vs{{.ID}}.{{.MessageTS}}equals{{.ID}}exceptfor a slash command, where it is empty and
{{.ID}}is the composite commandID.
{{.ID}}identifies the work item.{{.MessageTS}}is always a Slackmessage timestamp, meant to be passed to Slack APIs alongside
{{.ChannelID}}and
{{.ThreadTS}}. The reference docs explain both.CLI.
kelos get taskspawner <name>lists reaction triggers as:gear:beside the patterns in
Triggers.API versions.
reactionis added only tov1alpha2.v1alpha1triggers list. There they would read as empty triggers that fire on every bot
mention. They are kept, as
[{"name":"gear"}], in akelos.dev/v1alpha2-slack-reaction-triggersannotation instead.semantics, so the position change doesn't affect matching. The restored data
is re-validated against the field's markers, since the API server doesn't
validate conversion output.
Which issue(s) this PR is related to:
Related to #932 (Slack ChatOps sources).
Soemthing like: #1586 (scoring results by reaction) is a sibling that will share the
reaction_addedevent plumbing, even if we don't do 1586 specifically. Easier to extendSpecial notes for your reviewer:
Why this shape.
triggers. The list alone decides what fires thespawner, and empty
triggerskeeps its existing meaning.reactionis a block, not a scalar. Likely next options, such as who mayreact or a minimum count, go inside the block. They need no new CEL rule and
don't add pattern-only siblings to
SlackTrigger.Trade-offs of this shape.
triggerscap.trigger as
{}, which fires on every bot mention. So does any client builtagainst the older API types, such as an older
kelosCLI or a GitOpscontroller, when it reads and writes back a TaskSpawner. Every shape that puts
reactions under
slackhas this problem.docs/reference.mdsays to removereaction triggers before downgrading, or before managing these spawners with
such a client.
v1alpha1client that strips the preservationannotation drops the reaction triggers too, as it already does for
excludeFilters.Validation.
reaction.name: required, a lowercase emoji name without colons orwhitespace, max 100 characters. Uppercase is rejected because Slack sends
emoji names in lowercase, and loosening this later is backward-compatible.
triggersmax as constants inapi/v1alpha2/slackfilter_criteria.go, andmarker tests keep the two in sync.
Slack app setup. Reactions arrive only after the Slack app subscribes to
the
reaction_addedbot event and is granted thereactions:readscope, thenreinstalled. Without that, no events arrive and nothing errors. The new
"Slack reactions" section in
docs/reference.mdsays so.Tests.
excludeFilters,excludePatterns;{}beside a reaction trigger firing on every mention.thread_tsequals its own
ts) keeping an empty{{.ThreadTS}};found;
outside the allowlist.
second Task.
missing name, and restoring past the
triggerscap.name,
:gear:,Gear, andreactioncombined withpatternormentionOptional.make verify,make testandmake test-integrationpass.Does this PR introduce a user-facing change?