mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
109 lines
3.3 KiB
Go
109 lines
3.3 KiB
Go
// Package identity provides unified user identity utilities for PicoClaw.
|
|
// It introduces a canonical "platform:id" format and matching logic
|
|
// that is backward-compatible with all legacy allow-list formats.
|
|
package identity
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
)
|
|
|
|
// BuildCanonicalID constructs a canonical "platform:id" identifier.
|
|
// Both platform and platformID are lowercased and trimmed.
|
|
func BuildCanonicalID(platform, platformID string) string {
|
|
p := strings.ToLower(strings.TrimSpace(platform))
|
|
id := strings.TrimSpace(platformID)
|
|
if p == "" || id == "" {
|
|
return ""
|
|
}
|
|
return p + ":" + id
|
|
}
|
|
|
|
// ParseCanonicalID splits a canonical ID ("platform:id") into its parts.
|
|
// Returns ok=false if the input does not contain a colon separator.
|
|
func ParseCanonicalID(canonical string) (platform, id string, ok bool) {
|
|
canonical = strings.TrimSpace(canonical)
|
|
idx := strings.Index(canonical, ":")
|
|
if idx <= 0 || idx == len(canonical)-1 {
|
|
return "", "", false
|
|
}
|
|
return canonical[:idx], canonical[idx+1:], true
|
|
}
|
|
|
|
// MatchAllowed checks whether the given sender matches a single allow-list entry.
|
|
// It is backward-compatible with all legacy formats:
|
|
//
|
|
// - "123456" → matches sender.PlatformID
|
|
// - "@alice" → matches sender.Username
|
|
// - "123456|alice" → matches PlatformID or Username
|
|
// - "telegram:123456" → exact match on sender.CanonicalID
|
|
func MatchAllowed(sender bus.SenderInfo, allowed string) bool {
|
|
allowed = strings.TrimSpace(allowed)
|
|
if allowed == "" {
|
|
return false
|
|
}
|
|
|
|
// Try canonical match first: "platform:id" format
|
|
if platform, id, ok := ParseCanonicalID(allowed); ok {
|
|
// Only treat as canonical if the platform portion looks like a known platform name
|
|
// (not a pure-numeric string, which could be a compound ID)
|
|
if !isNumeric(platform) {
|
|
candidate := BuildCanonicalID(platform, id)
|
|
if candidate != "" && sender.CanonicalID != "" {
|
|
return strings.EqualFold(sender.CanonicalID, candidate)
|
|
}
|
|
// If sender has no canonical ID, try matching platform + platformID
|
|
return strings.EqualFold(platform, sender.Platform) &&
|
|
sender.PlatformID == id
|
|
}
|
|
}
|
|
|
|
// Keep track of explicit username format
|
|
isAtUsername := strings.HasPrefix(allowed, "@")
|
|
|
|
// Strip leading "@" for username matching
|
|
trimmed := strings.TrimPrefix(allowed, "@")
|
|
|
|
// Split compound "id|username" format
|
|
allowedID := trimmed
|
|
allowedUser := ""
|
|
if idx := strings.Index(trimmed, "|"); idx > 0 {
|
|
allowedID = trimmed[:idx]
|
|
allowedUser = trimmed[idx+1:]
|
|
}
|
|
|
|
// Match against PlatformID
|
|
if sender.PlatformID != "" && sender.PlatformID == allowedID {
|
|
return true
|
|
}
|
|
|
|
// Match against Username only when explicitly requested via "@username"
|
|
if isAtUsername && sender.Username != "" && sender.Username == trimmed {
|
|
return true
|
|
}
|
|
|
|
// Match compound sender format against allowed parts
|
|
if allowedUser != "" && sender.PlatformID != "" && sender.PlatformID == allowedID {
|
|
return true
|
|
}
|
|
if allowedUser != "" && sender.Username != "" && sender.Username == allowedUser {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// isNumeric returns true if s consists entirely of digits.
|
|
func isNumeric(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
for _, r := range s {
|
|
if r < '0' || r > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|