package acl import ( "strings" "time" "github.com/memohai/memoh/internal/channel" ) const ( ActionChatTrigger = "chat.trigger" EffectAllow = "allow" EffectDeny = "deny" SubjectKindAll = "all" SubjectKindChannelIdentity = "channel_identity" SubjectKindChannelType = "channel_type" ) // Rule is the full ACL rule record returned to callers. type Rule struct { ID string `json:"id"` BotID string `json:"bot_id"` Priority int32 `json:"priority"` Enabled bool `json:"enabled"` Description string `json:"description,omitempty"` Action string `json:"action"` Effect string `json:"effect"` SubjectKind string `json:"subject_kind"` ChannelIdentityID string `json:"channel_identity_id,omitempty"` SubjectChannelType string `json:"subject_channel_type,omitempty"` SourceScope *SourceScope `json:"source_scope,omitempty"` ChannelType string `json:"channel_type,omitempty"` ChannelSubjectID string `json:"channel_subject_id,omitempty"` ChannelIdentityDisplayName string `json:"channel_identity_display_name,omitempty"` ChannelIdentityAvatarURL string `json:"channel_identity_avatar_url,omitempty"` LinkedUserID string `json:"linked_user_id,omitempty"` LinkedUserUsername string `json:"linked_user_username,omitempty"` LinkedUserDisplayName string `json:"linked_user_display_name,omitempty"` LinkedUserAvatarURL string `json:"linked_user_avatar_url,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type ListRulesResponse struct { Items []Rule `json:"items"` } type DefaultEffectResponse struct { DefaultEffect string `json:"default_effect"` } // SourceScope narrows a rule to a specific conversation / thread. // Any zero-value field means "match any". // Channel filtering is handled at the subject level (channel_type / channel_identity). type SourceScope struct { ConversationType string `json:"conversation_type,omitempty"` ConversationID string `json:"conversation_id,omitempty"` ThreadID string `json:"thread_id,omitempty"` } // CreateRuleRequest is used to create a new ACL rule. type CreateRuleRequest struct { Priority int32 `json:"priority"` Enabled bool `json:"enabled"` Description string `json:"description,omitempty"` Effect string `json:"effect"` SubjectKind string `json:"subject_kind"` ChannelIdentityID string `json:"channel_identity_id,omitempty"` SubjectChannelType string `json:"subject_channel_type,omitempty"` SourceScope *SourceScope `json:"source_scope,omitempty"` } // UpdateRuleRequest is used to update an existing ACL rule. type UpdateRuleRequest struct { Priority int32 `json:"priority"` Enabled bool `json:"enabled"` Description string `json:"description,omitempty"` Effect string `json:"effect"` SubjectKind string `json:"subject_kind"` ChannelIdentityID string `json:"channel_identity_id,omitempty"` SubjectChannelType string `json:"subject_channel_type,omitempty"` SourceScope *SourceScope `json:"source_scope,omitempty"` } // ReorderItem is a single priority update in a batch reorder request. type ReorderItem struct { ID string `json:"id"` Priority int32 `json:"priority"` } type ReorderRequest struct { Items []ReorderItem `json:"items"` } // EvaluateRequest carries all context needed to evaluate a chat.trigger. type EvaluateRequest struct { BotID string ChannelIdentityID string ChannelType string SourceScope SourceScope } type ChannelIdentityCandidate struct { ID string `json:"id"` Channel string `json:"channel"` ChannelSubjectID string `json:"channel_subject_id"` DisplayName string `json:"display_name,omitempty"` AvatarURL string `json:"avatar_url,omitempty"` LinkedUserID string `json:"linked_user_id,omitempty"` LinkedUsername string `json:"linked_username,omitempty"` LinkedDisplayName string `json:"linked_display_name,omitempty"` LinkedAvatarURL string `json:"linked_avatar_url,omitempty"` } type ChannelIdentityCandidateListResponse struct { Items []ChannelIdentityCandidate `json:"items"` } type ObservedConversationCandidate struct { RouteID string `json:"route_id"` Channel string `json:"channel"` ConversationType string `json:"conversation_type,omitempty"` ConversationID string `json:"conversation_id"` ThreadID string `json:"thread_id,omitempty"` ConversationName string `json:"conversation_name,omitempty"` LastObservedAt time.Time `json:"last_observed_at"` } type ObservedConversationCandidateListResponse struct { Items []ObservedConversationCandidate `json:"items"` } func (s SourceScope) Normalize() SourceScope { scope := SourceScope{ ConversationID: strings.TrimSpace(s.ConversationID), ThreadID: strings.TrimSpace(s.ThreadID), } if raw := strings.TrimSpace(s.ConversationType); raw != "" { scope.ConversationType = channel.NormalizeConversationType(raw) } if scope.ThreadID != "" && scope.ConversationType == "" { scope.ConversationType = channel.ConversationTypeThread } return scope } func (s SourceScope) IsZero() bool { return strings.TrimSpace(s.ConversationType) == "" && strings.TrimSpace(s.ConversationID) == "" && strings.TrimSpace(s.ThreadID) == "" }