// Package session maps IM channels to internal sessions and resolves the // strongly typed actor identity that gates every downstream permission decision. // // It is the M6 home of three cross-cutting concerns wired together at the // channel layer, per DevDocs/design/11 §0/§2: // // - Channel↔session mapping: a persistent (platform, channel_id) → session_id // map (SQLite table channel_sessions), so the same conversation always maps // to the same internal session across restarts. // - Owner recognition: a structural, never-prompt rule. ResolveActor decides // owner vs group_member vs stranger purely from channel-level actor metadata // matched against the user.OwnerProfile account whitelist. Nothing here reads // message text to infer identity — that is a project red line. // - Proactive push policy: per-platform rules from design/08 §3 (weixin-ilink // 24h window, OneBot burst rate limit, telegram/webhook unrestricted), with a // conservative strict limit as the default for unknown platforms (裁决 06.4.4). // // The package is concurrency-safe: the manager guards its cache with an // RWMutex and the rate limiter uses its own mutex, so multiple adapters may // resolve sessions and issue pushes concurrently. package session import ( "crypto/rand" "encoding/hex" "errors" "fmt" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) // Package-level sentinel errors. Each is wrapped with %w at the call site so // callers can use errors.Is. var ( // ErrNilDB is returned when SessionManager is built with a nil database. ErrNilDB = errors.New("session: nil database handle") // ErrInvalidChannel is returned when a message has no usable channel id. ErrInvalidChannel = errors.New("session: empty channel id") ) // sessionIDPrefix distinguishes generated session ids from foreign identifiers. const sessionIDPrefix = "sess-" // Session is the resolved channel↔session binding for one inbound message. It // carries the identity metadata (ActorRole, Owner, IsGroup) that the policy // engine and memory layer read, plus the isolation flag MemWritable which says // whether this conversation may write to shared memory. // // The memory rule follows design/02 §3 and design/11 §2: only the owner's // private conversation may read/write memory shared with the Web main session; // group chats and stranger conversations may read memory read-only and may // never persist anything that could leak owner-sensitive information. type Session struct { // Platform is the normalized platform identifier (e.g. "weixin-ilink"). Platform string // ChannelID is the stable channel key used for session mapping. ChannelID string // SessionID is the internal session binding the channel maps to. SessionID string // ActorRole is the strongly typed role of the resolved actor. ActorRole adapter.Role // Owner reports whether the actor is the bound owner account. Owner bool // IsGroup reports whether this is a group/room conversation. IsGroup bool // MemWritable reports whether this conversation may write shared memory. // It is true only for the owner's private conversation. MemWritable bool } // newSessionID returns a random, collision-resistant session identifier. func newSessionID() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { // rand.Read should never fail; fall back to a nanosecond-named id so a // session can still be created rather than failing the whole call. return fmt.Sprintf("%s%d", sessionIDPrefix, time.Now().UnixNano()) } return sessionIDPrefix + hex.EncodeToString(b[:]) }