mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
7f9d6e4aba
Backend - New subject kinds: all / channel_identity / channel_type - Source scope fields on bot_acl_rules: source_channel, source_conversation_type, source_conversation_id, source_thread_id - Fix source_scope_check constraint: resolve source_channel server-side (channel_type → subject_channel_type; channel_identity → DB lookup) - Add GET /bots/:id/acl/channel-types/:type/conversations to list observed conversations by platform type - ListObservedConversations: include private/DM chats, normalise conversation_type; COALESCE(name, handle) for display name - enrichConversationAvatar: persist entry.Name → conversation_name (keeps Telegram group titles current on every message) - Unify Priority type to int32 across Go types to match DB INTEGER; remove all int/int32 casts in service layer - Fix duplicate nil guard in Evaluate; drop dead SourceScope.Channel field - Migration 0048_acl_redesign Frontend - Drag-and-drop rule priority reordering (SortableJS/useSortable); fix reorder: compute new order from oldIndex/newIndex directly, not from the array (which useSortable syncs after onEnd) - Conversation scope selector: searchable popover backed by observed conversations (by identity or platform type); collapsible manual-ID fallback - Display: name as primary label, stable channel·type·id always shown as subtitle for verification - bot-terminal: accessibility fix on close-tab button (keyboard events) - i18n: drag-to-reorder, conversation source, manual IDs (en/zh) Tests: update fakeChatACL to Evaluate interface; fix SourceScope literals. SDK/spec regenerated.
154 lines
5.7 KiB
Go
154 lines
5.7 KiB
Go
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) == ""
|
|
}
|