fix: improve ack reaction logic and Feishu message handling
- Fix group-mentions scope to properly check IsGroup - Add allowlist check in Feishu message handler - Fix mention detection to only apply in direct chats - Remove remove_ack_after_reply configuration option - Change default ack_reaction to empty string (disabled by default) - Add comprehensive unit tests for ack_reactions
This commit is contained in:
parent
093d24f9ab
commit
cdc092475a
5 changed files with 219 additions and 21 deletions
|
|
@ -130,9 +130,8 @@
|
||||||
"monitor_usb": true
|
"monitor_usb": true
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"ack_reaction": "OK",
|
"ack_reaction": "",
|
||||||
"ack_reaction_scope": "group-mentions",
|
"ack_reaction_scope": "group-mentions"
|
||||||
"remove_ack_after_reply": false
|
|
||||||
},
|
},
|
||||||
"gateway": {
|
"gateway": {
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ func ShouldAckReaction(params AckReactionParams) bool {
|
||||||
|
|
||||||
// Group mentions only
|
// Group mentions only
|
||||||
if scope == AckReactionScopeGroupMentions {
|
if scope == AckReactionScopeGroupMentions {
|
||||||
|
// Not a group message, don't ack
|
||||||
|
if !params.IsGroup {
|
||||||
|
return false
|
||||||
|
}
|
||||||
// Not a mentionable group, don't ack
|
// Not a mentionable group, don't ack
|
||||||
if !params.IsMentionableGroup {
|
if !params.IsMentionableGroup {
|
||||||
return false
|
return false
|
||||||
|
|
@ -90,8 +94,6 @@ func ShouldAckReaction(params AckReactionParams) bool {
|
||||||
|
|
||||||
// AckReactionManager manages the lifecycle of acknowledgment reactions
|
// AckReactionManager manages the lifecycle of acknowledgment reactions
|
||||||
type AckReactionManager struct {
|
type AckReactionManager struct {
|
||||||
// RemoveAfterReply indicates whether to remove the ack after reply
|
|
||||||
RemoveAfterReply bool
|
|
||||||
// ReactionValue is the current reaction value (emoji)
|
// ReactionValue is the current reaction value (emoji)
|
||||||
ReactionValue string
|
ReactionValue string
|
||||||
// Added indicates if the ack reaction has been added
|
// Added indicates if the ack reaction has been added
|
||||||
|
|
@ -99,11 +101,10 @@ type AckReactionManager struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAckReactionManager creates a new ack reaction manager
|
// NewAckReactionManager creates a new ack reaction manager
|
||||||
func NewAckReactionManager(removeAfterReply bool, reaction string) *AckReactionManager {
|
func NewAckReactionManager(reaction string) *AckReactionManager {
|
||||||
return &AckReactionManager{
|
return &AckReactionManager{
|
||||||
RemoveAfterReply: removeAfterReply,
|
ReactionValue: reaction,
|
||||||
ReactionValue: reaction,
|
Added: false,
|
||||||
Added: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,8 +112,3 @@ func NewAckReactionManager(removeAfterReply bool, reaction string) *AckReactionM
|
||||||
func (m *AckReactionManager) MarkAdded() {
|
func (m *AckReactionManager) MarkAdded() {
|
||||||
m.Added = true
|
m.Added = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldRemoveAfterReply determines whether to remove the ack after reply
|
|
||||||
func (m *AckReactionManager) ShouldRemoveAfterReply() bool {
|
|
||||||
return m.RemoveAfterReply && m.Added && m.ReactionValue != ""
|
|
||||||
}
|
|
||||||
|
|
|
||||||
196
pkg/channels/ack_reactions_test.go
Normal file
196
pkg/channels/ack_reactions_test.go
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
package channels
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestShouldAckReaction(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
params AckReactionParams
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
// Scope: off/none (disabled)
|
||||||
|
{
|
||||||
|
name: "scope off disables all",
|
||||||
|
params: AckReactionParams{Scope: AckReactionScopeOff, IsDirect: true, IsGroup: true},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope none alias disables all",
|
||||||
|
params: AckReactionParams{Scope: AckReactionScopeNone, IsDirect: true, IsGroup: true},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
// Scope: all
|
||||||
|
{
|
||||||
|
name: "scope all enables for direct messages",
|
||||||
|
params: AckReactionParams{Scope: AckReactionScopeAll, IsDirect: true, IsGroup: false},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope all enables for group messages",
|
||||||
|
params: AckReactionParams{Scope: AckReactionScopeAll, IsDirect: false, IsGroup: true},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
// Scope: direct
|
||||||
|
{
|
||||||
|
name: "scope direct enables for direct messages",
|
||||||
|
params: AckReactionParams{Scope: AckReactionScopeDirect, IsDirect: true, IsGroup: false},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope direct disables for group messages",
|
||||||
|
params: AckReactionParams{Scope: AckReactionScopeDirect, IsDirect: false, IsGroup: true},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
// Scope: group-all
|
||||||
|
{
|
||||||
|
name: "scope group-all enables for group messages",
|
||||||
|
params: AckReactionParams{Scope: AckReactionScopeGroupAll, IsDirect: false, IsGroup: true},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope group-all disables for direct messages",
|
||||||
|
params: AckReactionParams{Scope: AckReactionScopeGroupAll, IsDirect: true, IsGroup: false},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
// Scope: group-mentions (mentionable group, require mention, can detect)
|
||||||
|
{
|
||||||
|
name: "scope group-mentions with all conditions met",
|
||||||
|
params: AckReactionParams{
|
||||||
|
Scope: AckReactionScopeGroupMentions,
|
||||||
|
IsDirect: false,
|
||||||
|
IsGroup: true,
|
||||||
|
IsMentionableGroup: true,
|
||||||
|
RequireMention: true,
|
||||||
|
CanDetectMention: true,
|
||||||
|
WasMentioned: true,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope group-mentions bypass mention requirement",
|
||||||
|
params: AckReactionParams{
|
||||||
|
Scope: AckReactionScopeGroupMentions,
|
||||||
|
IsDirect: false,
|
||||||
|
IsGroup: true,
|
||||||
|
IsMentionableGroup: true,
|
||||||
|
RequireMention: true,
|
||||||
|
CanDetectMention: true,
|
||||||
|
ShouldBypassMention: true,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope group-mentions not in group",
|
||||||
|
params: AckReactionParams{
|
||||||
|
Scope: AckReactionScopeGroupMentions,
|
||||||
|
IsDirect: true,
|
||||||
|
IsGroup: false,
|
||||||
|
IsMentionableGroup: true,
|
||||||
|
RequireMention: true,
|
||||||
|
CanDetectMention: true,
|
||||||
|
WasMentioned: true,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope group-mentions not mentionable group",
|
||||||
|
params: AckReactionParams{
|
||||||
|
Scope: AckReactionScopeGroupMentions,
|
||||||
|
IsDirect: false,
|
||||||
|
IsGroup: true,
|
||||||
|
IsMentionableGroup: false,
|
||||||
|
RequireMention: true,
|
||||||
|
CanDetectMention: true,
|
||||||
|
WasMentioned: true,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope group-mentions no mention required",
|
||||||
|
params: AckReactionParams{
|
||||||
|
Scope: AckReactionScopeGroupMentions,
|
||||||
|
IsDirect: false,
|
||||||
|
IsGroup: true,
|
||||||
|
IsMentionableGroup: true,
|
||||||
|
RequireMention: false,
|
||||||
|
CanDetectMention: true,
|
||||||
|
WasMentioned: true,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope group-mentions cannot detect mention",
|
||||||
|
params: AckReactionParams{
|
||||||
|
Scope: AckReactionScopeGroupMentions,
|
||||||
|
IsDirect: false,
|
||||||
|
IsGroup: true,
|
||||||
|
IsMentionableGroup: true,
|
||||||
|
RequireMention: true,
|
||||||
|
CanDetectMention: false,
|
||||||
|
WasMentioned: true,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scope group-mentions not mentioned",
|
||||||
|
params: AckReactionParams{
|
||||||
|
Scope: AckReactionScopeGroupMentions,
|
||||||
|
IsDirect: false,
|
||||||
|
IsGroup: true,
|
||||||
|
IsMentionableGroup: true,
|
||||||
|
RequireMention: true,
|
||||||
|
CanDetectMention: true,
|
||||||
|
WasMentioned: false,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
// Default scope (empty) - should default to group-mentions
|
||||||
|
{
|
||||||
|
name: "empty scope defaults to group-mentions with all conditions met",
|
||||||
|
params: AckReactionParams{
|
||||||
|
Scope: "",
|
||||||
|
IsDirect: false,
|
||||||
|
IsGroup: true,
|
||||||
|
IsMentionableGroup: true,
|
||||||
|
RequireMention: true,
|
||||||
|
CanDetectMention: true,
|
||||||
|
WasMentioned: true,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty scope defaults to group-mentions not in group",
|
||||||
|
params: AckReactionParams{
|
||||||
|
Scope: "",
|
||||||
|
IsDirect: true,
|
||||||
|
IsGroup: false,
|
||||||
|
IsMentionableGroup: true,
|
||||||
|
RequireMention: true,
|
||||||
|
CanDetectMention: true,
|
||||||
|
WasMentioned: true,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := ShouldAckReaction(tt.params); got != tt.want {
|
||||||
|
t.Errorf("ShouldAckReaction() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAckReactionManager(t *testing.T) {
|
||||||
|
t.Run("mark added", func(t *testing.T) {
|
||||||
|
m := NewAckReactionManager("OK")
|
||||||
|
if m.Added {
|
||||||
|
t.Error("expected Added to be false initially")
|
||||||
|
}
|
||||||
|
m.MarkAdded()
|
||||||
|
if !m.Added {
|
||||||
|
t.Error("expected Added to be true after MarkAdded")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -151,14 +151,24 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim.
|
||||||
content = "[empty message]"
|
content = "[empty message]"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check allowlist before processing
|
||||||
|
if !c.IsAllowed(senderID) {
|
||||||
|
logger.DebugCF("feishu", "Message blocked by allowlist", map[string]interface{}{
|
||||||
|
"sender_id": senderID,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Determine chat type: p2p = direct, group = group chat
|
// Determine chat type: p2p = direct, group = group chat
|
||||||
chatType := stringValue(message.ChatType)
|
chatType := stringValue(message.ChatType)
|
||||||
isGroup := chatType == "group"
|
isGroup := chatType == "group"
|
||||||
isDirect := chatType == "p2p"
|
isDirect := chatType == "p2p"
|
||||||
|
|
||||||
// Check if bot was mentioned
|
// Check if bot was mentioned
|
||||||
|
// Without a reliable bot ID in scope, we conservatively only
|
||||||
|
// treat mentions in direct (p2p) chats as relevant to the bot.
|
||||||
wasMentioned := false
|
wasMentioned := false
|
||||||
if message.Mentions != nil && len(message.Mentions) > 0 {
|
if isDirect && message.Mentions != nil && len(message.Mentions) > 0 {
|
||||||
wasMentioned = true
|
wasMentioned = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,10 +171,8 @@ type DevicesConfig struct {
|
||||||
type MessagesConfig struct {
|
type MessagesConfig struct {
|
||||||
// AckReaction is the emoji used to acknowledge inbound messages (empty to disable)
|
// AckReaction is the emoji used to acknowledge inbound messages (empty to disable)
|
||||||
AckReaction string `json:"ack_reaction" env:"PICOCLAW_MESSAGES_ACK_REACTION"`
|
AckReaction string `json:"ack_reaction" env:"PICOCLAW_MESSAGES_ACK_REACTION"`
|
||||||
// AckReactionScope controls when to send ack reactions: "all", "direct", "group-all", "group-mentions", "off"
|
// AckReactionScope controls when to send ack reactions: "all", "direct", "group-all", "group-mentions", "off" (or "none" to disable)
|
||||||
AckReactionScope string `json:"ack_reaction_scope" env:"PICOCLAW_MESSAGES_ACK_REACTION_SCOPE"`
|
AckReactionScope string `json:"ack_reaction_scope" env:"PICOCLAW_MESSAGES_ACK_REACTION_SCOPE"`
|
||||||
// RemoveAckAfterReply removes the ack reaction after reply is sent
|
|
||||||
RemoveAckAfterReply bool `json:"remove_ack_after_reply" env:"PICOCLAW_MESSAGES_REMOVE_ACK_AFTER_REPLY"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProvidersConfig struct {
|
type ProvidersConfig struct {
|
||||||
|
|
@ -343,9 +341,8 @@ func DefaultConfig() *Config {
|
||||||
MonitorUSB: true,
|
MonitorUSB: true,
|
||||||
},
|
},
|
||||||
Messages: MessagesConfig{
|
Messages: MessagesConfig{
|
||||||
AckReaction: "OK",
|
AckReaction: "",
|
||||||
AckReactionScope: "group-mentions",
|
AckReactionScope: "group-mentions",
|
||||||
RemoveAckAfterReply: false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue