81 lines
3.5 KiB
Go
81 lines
3.5 KiB
Go
// 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).
|
|
//
|
|
// 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[:])
|
|
}
|