| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453 |
- package state
- import (
- "errors"
- "fmt"
- "net/netip"
- "slices"
- "sync"
- "time"
- "github.com/mk6i/open-oscar-server/wire"
- )
- // SessSendStatus is the result of sending a message to a user.
- type SessSendStatus int
- // RateClassState tracks the rate limiting state for a specific rate class
- // within a user's session.
- //
- // It embeds the static wire.RateClass configuration and maintains dynamic,
- // per-session state used to evaluate rate limits in real time.
- type RateClassState struct {
- // static rate limit configuration for this class
- wire.RateClass
- // CurrentLevel is the current exponential moving average for this rate
- // class.
- CurrentLevel int32
- // LastTime represents the last time a SNAC message was sent for this rate
- // class.
- LastTime time.Time
- // CurrentStatus is the last recorded rate limit status for this rate class.
- CurrentStatus wire.RateLimitStatus
- // Subscribed indicates whether the user wants to receive rate limit
- // parameter updates for this rate class.
- Subscribed bool
- // LimitedNow indicates whether the user is currently rate limited for this
- // rate class; the user is blocked from sending SNACs in this rate class
- // until the clear threshold is met.
- LimitedNow bool
- }
- const (
- // SessSendOK indicates message was sent to recipient
- SessSendOK SessSendStatus = iota
- // SessSendClosed indicates send did not complete because session is closed
- SessSendClosed
- // SessQueueFull indicates send failed due to full queue -- client is likely
- // dead
- SessQueueFull
- )
- // maxNotifyTxnNames is the maximum number of distinct screen names accumulated in
- // a single notify transaction.
- const maxNotifyTxnNames = 1000
- var (
- errNotifyTxnNotActive = errors.New("notify transaction is not active")
- errNotifyTxnTooManyNames = errors.New("notify transaction exceeds maximum screen names")
- )
- // Session represents shared user-level state that persists across all concurrent
- // connections for a single user account.
- //
- // Session maintains client identity information, preferences, rate limiting state,
- // and other shared data that should be consistent across all of a user's active
- // connections. Individual connection-specific state (like remote address, sign-on
- // status, or per-connection capabilities) is stored in SessionInstance instead.
- //
- // All methods on Session are safe for concurrent use.
- type Session struct {
- mutex sync.RWMutex
- // User identity (shared across all sessions)
- displayScreenName DisplayScreenName
- identScreenName IdentScreenName
- uin uint32
- memberSince time.Time
- signonTime time.Time
- // User-level settings and profile (shared)
- warning uint16
- warningCh chan uint16
- offlineMsgCount int
- chatRoomCookie string
- buddyIcon wire.BARTID
- typingEventsEnabled bool
- // usesFeedbag is true after FeedbagService.Use completes this sign-on (server-side buddy list / SSI).
- usesFeedbag bool
- // Rate limiting (shared across all sessions per user)
- rateLimitStates [5]RateClassState
- rateLimitStatesOriginal [5]RateClassState
- lastObservedStates [5]RateClassState
- instances map[uint8]*SessionInstance
- instancesOrdered []*SessionInstance
- initOnce sync.Once
- onSessCloseFn func()
- nowFn func() time.Time
- }
- // NewSession creates a new Session for a user.
- func NewSession() *Session {
- return &Session{
- warningCh: make(chan uint16, 1),
- instances: make(map[uint8]*SessionInstance),
- instancesOrdered: make([]*SessionInstance, 0),
- onSessCloseFn: func() {},
- nowFn: time.Now,
- }
- }
- //
- // Instance Management
- //
- // AddInstance creates and adds a new connection instance to the session.
- // Returns the newly created SessionInstance with a unique instance number.
- func (s *Session) AddInstance() *SessionInstance {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- instance := &SessionInstance{
- session: s,
- instanceNum: s.generateInstanceNum(),
- msgCh: make(chan wire.SNACMessage, 1000),
- stopCh: make(chan struct{}),
- capabilities: make([][16]byte, 0),
- foodGroupVersions: defaultFoodGroupVersions(),
- userInfoBitmask: wire.OServiceUserFlagOSCARFree,
- userStatusBitmask: wire.OServiceUserStatusAvailable,
- onInstanceCloseFn: func() {},
- }
- s.instances[instance.instanceNum] = instance
- s.instancesOrdered = append(s.instancesOrdered, instance)
- return instance
- }
- // HasLiveInstances returns true if the session has at least one live instance.
- // A live instance is one that is not closed and has completed the sign-on sequence.
- func (s *Session) HasLiveInstances() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- for _, instance := range s.instances {
- if instance.live() {
- return true
- }
- }
- return false
- }
- // InstanceCount returns the number of total instances in the session group.
- func (s *Session) InstanceCount() int {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return len(s.instances)
- }
- // Instances returns all instances in the order they were added.
- func (s *Session) Instances() []*SessionInstance {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- instances := make([]*SessionInstance, len(s.instancesOrdered))
- copy(instances, s.instancesOrdered)
- return instances
- }
- // RemoveInstance removes an instance from the session group.
- func (s *Session) RemoveInstance(instance *SessionInstance) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- delete(s.instances, instance.instanceNum)
- for i, inst := range s.instancesOrdered {
- if inst == instance {
- s.instancesOrdered = append(s.instancesOrdered[:i], s.instancesOrdered[i+1:]...)
- break
- }
- }
- }
- // generateInstanceNum generates the next available instance number for this session group.
- // It finds the next number that is not currently in use by iterating over the possible key range.
- func (s *Session) generateInstanceNum() uint8 {
- // if num reaches 0, all number have been taken
- for num := uint8(1); num != 0; num++ {
- if _, exists := s.instances[num]; !exists {
- return num
- }
- }
- // the caller should ensure there are no more than 255 instances per session
- panic("all instance numbers are taken (max 255 instances per session)")
- }
- // defaultFoodGroupVersions returns default version numbers for all food groups.
- func defaultFoodGroupVersions() [wire.MDir + 1]uint16 {
- vals := [wire.MDir + 1]uint16{}
- vals[wire.OService] = 1
- vals[wire.Locate] = 1
- vals[wire.Buddy] = 1
- vals[wire.ICBM] = 1
- vals[wire.Advert] = 1
- vals[wire.Invite] = 1
- vals[wire.Admin] = 1
- vals[wire.Popup] = 1
- vals[wire.PermitDeny] = 1
- vals[wire.UserLookup] = 1
- vals[wire.Stats] = 1
- vals[wire.Translate] = 1
- vals[wire.ChatNav] = 1
- vals[wire.Chat] = 1
- vals[wire.ODir] = 1
- vals[wire.BART] = 1
- vals[wire.Feedbag] = 1
- vals[wire.ICQ] = 1
- vals[wire.BUCP] = 1
- vals[wire.Alert] = 1
- vals[wire.Plugin] = 1
- vals[wire.UnnamedFG24] = 1
- vals[wire.MDir] = 1
- return vals
- }
- //
- // Identity
- //
- // ChatRoomCookie returns the chat room cookie.
- func (s *Session) ChatRoomCookie() string {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.chatRoomCookie
- }
- // DisplayScreenName returns the user's display screen name.
- func (s *Session) DisplayScreenName() DisplayScreenName {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.displayScreenName
- }
- // IdentScreenName returns the user's identity screen name.
- func (s *Session) IdentScreenName() IdentScreenName {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.identScreenName
- }
- // SetDisplayScreenName sets the user's display screen name (shared across all sessions).
- func (s *Session) SetDisplayScreenName(displayScreenName DisplayScreenName) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.displayScreenName = displayScreenName
- }
- // SetIdentScreenName sets the user's identity screen name (shared across all sessions).
- func (s *Session) SetIdentScreenName(screenName IdentScreenName) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.identScreenName = screenName
- }
- // SetUIN sets the user's ICQ number (shared across all sessions).
- func (s *Session) SetUIN(uin uint32) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.uin = uin
- }
- // UIN returns the user's ICQ number.
- func (s *Session) UIN() uint32 {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.uin
- }
- //
- // Status / Availability
- //
- // Away returns true if all instances are away.
- func (s *Session) Away() bool {
- instances := s.Instances()
- if len(instances) == 0 {
- return false
- }
- for _, instance := range instances {
- if instance.UserInfoBitmask()&wire.OServiceUserFlagUnavailable == 0 &&
- instance.UserStatusBitmask()&wire.OServiceUserStatusAway == 0 {
- return false
- }
- }
- return true
- }
- // AwayMessage returns the away message from the last instance to set an away message.
- func (s *Session) AwayMessage() string {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- var latest *SessionInstance
- var latestTime time.Time
- for _, instance := range s.instances {
- // Only consider instances that are away
- if !instance.Away() {
- continue
- }
- _, awayTime := instance.AwayMessage()
- if latest == nil || awayTime.After(latestTime) {
- latest = instance
- latestTime = awayTime
- }
- }
- if latest == nil {
- return ""
- }
- awayMsg, _ := latest.AwayMessage()
- return awayMsg
- }
- // Idle returns true if all instances are idle.
- func (s *Session) Idle() bool {
- instances := s.Instances()
- if len(instances) == 0 {
- return false
- }
- for _, instance := range instances {
- if !instance.Idle() {
- return false
- }
- }
- return true
- }
- // IdleTime returns the latest idle time if all instances are idle. If not all
- // instances are idle, it returns a zero time.
- func (s *Session) IdleTime() time.Time {
- if !s.Idle() {
- return time.Time{}
- }
- return s.mostRecentIdleTime()
- }
- // Inactive returns true if all instances are not active.
- func (s *Session) Inactive() bool {
- for _, instance := range s.Instances() {
- if instance.active() {
- return false
- }
- }
- return true
- }
- // Instance returns the SessionInstance with the given instance number, or nil if not found.
- func (s *Session) Instance(num uint8) *SessionInstance {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.instances[num]
- }
- // Invisible returns true if all instances are invisible.
- func (s *Session) Invisible() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- for _, instance := range s.instances {
- if !instance.Invisible() {
- return false
- }
- }
- return true
- }
- // mostRecentIdleTime returns the most recent idle time from all instances.
- func (s *Session) mostRecentIdleTime() time.Time {
- var mostRecent time.Time
- for _, instance := range s.Instances() {
- if mostRecent.IsZero() || (instance.Idle() && instance.IdleTime().After(mostRecent)) {
- mostRecent = instance.IdleTime()
- }
- }
- return mostRecent
- }
- //
- // Rate Limiting / Warning
- //
- // EvaluateRateLimit checks and updates the rate limit state.
- func (s *Session) EvaluateRateLimit(now time.Time, rateClassID wire.RateLimitClassID) wire.RateLimitStatus {
- if s.AllUserInfoBitmask(wire.OServiceUserFlagBot) {
- return wire.RateLimitStatusClear // don't rate limit bots
- }
- s.mutex.Lock()
- rateClass := &s.rateLimitStates[rateClassID-1]
- status, newLevel := wire.CheckRateLimit(rateClass.LastTime, now, rateClass.RateClass, rateClass.CurrentLevel, rateClass.LimitedNow)
- rateClass.CurrentLevel = newLevel
- rateClass.CurrentStatus = status
- rateClass.LastTime = now
- rateClass.LimitedNow = status == wire.RateLimitStatusLimited
- s.mutex.Unlock()
- if status == wire.RateLimitStatusDisconnect {
- s.CloseSession()
- }
- return status
- }
- // ObserveRateChanges updates rate limit states and returns changes.
- func (s *Session) ObserveRateChanges(now time.Time) (classDelta []RateClassState, stateDelta []RateClassState) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- for i, params := range s.rateLimitStates {
- if !params.Subscribed {
- continue
- }
- state, level := wire.CheckRateLimit(params.LastTime, now, params.RateClass, params.CurrentLevel, params.LimitedNow)
- s.rateLimitStates[i].CurrentStatus = state
- // clear limited now flag if passing from limited state to clear state
- if s.rateLimitStates[i].LimitedNow && state == wire.RateLimitStatusClear {
- s.rateLimitStates[i].LimitedNow = false
- s.rateLimitStates[i].CurrentLevel = level
- }
- // did rate class change?
- if params.RateClass != s.lastObservedStates[i].RateClass {
- classDelta = append(classDelta, s.rateLimitStates[i])
- }
- // did rate limit status change?
- if s.lastObservedStates[i].CurrentStatus != s.rateLimitStates[i].CurrentStatus {
- stateDelta = append(stateDelta, s.rateLimitStates[i])
- }
- // save it for next time
- s.lastObservedStates[i] = s.rateLimitStates[i]
- }
- return classDelta, stateDelta
- }
- // RateLimitStates returns the current rate limit states (shared across all sessions).
- func (s *Session) RateLimitStates() [5]RateClassState {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.rateLimitStates
- }
- // SetRateClasses sets the rate limit classes (shared across all sessions).
- func (s *Session) SetRateClasses(now time.Time, classes wire.RateLimitClasses) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- var newStates [5]RateClassState
- for i, class := range classes.All() {
- newStates[i] = RateClassState{
- CurrentLevel: class.MaxLevel,
- CurrentStatus: wire.RateLimitStatusClear,
- LastTime: now,
- RateClass: class,
- Subscribed: s.lastObservedStates[i].Subscribed,
- }
- }
- if s.lastObservedStates[0].ID == 0 {
- s.lastObservedStates = newStates
- } else {
- s.lastObservedStates = s.rateLimitStates
- }
- s.rateLimitStates = newStates
- s.rateLimitStatesOriginal = newStates
- }
- // SetWarning sets the user's warning level (shared across all sessions).
- func (s *Session) SetWarning(warning uint16) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.warning = warning
- }
- // ScaleWarningAndRateLimit increments the user's warning level and scales rate limits.
- func (s *Session) ScaleWarningAndRateLimit(incr int16, classID wire.RateLimitClassID) (bool, uint16) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- // Handle warning level increment
- newWarning := int32(s.warning) + int32(incr)
- if newWarning > 1000 {
- return false, 0
- }
- if newWarning < 0 {
- s.warning = 0 // clamp min at 0
- } else {
- s.warning = uint16(newWarning)
- }
- pct := float32(incr) / 1000.0
- // create reference variables for better readability
- rateClass := &s.rateLimitStates[classID-1]
- originalRateClass := &s.rateLimitStatesOriginal[classID-1]
- // clamp function to constrain values between min and max
- clamp := func(value, min, max int32) int32 {
- if value < min {
- return min
- }
- if value > max {
- return max
- }
- return value
- }
- // Apply a buffer to limit/clear/alert levels so that they never approach
- // too close to the maximum level. Otherwise, AIM 4.8 exhibits instability
- // (client crashes, IM window glitches) when the warning level reaches 90-100%.
- maxLevel := originalRateClass.MaxLevel - 150
- // scale the rate limit parameters
- newLimitLevel := rateClass.LimitLevel + int32(float32(maxLevel-originalRateClass.LimitLevel)*pct)
- rateClass.LimitLevel = clamp(newLimitLevel, originalRateClass.LimitLevel, originalRateClass.MaxLevel)
- newLimitLevel = rateClass.ClearLevel + int32(float32(maxLevel-originalRateClass.ClearLevel)*pct)
- rateClass.ClearLevel = clamp(newLimitLevel, originalRateClass.ClearLevel, originalRateClass.MaxLevel)
- newLimitLevel = rateClass.AlertLevel + int32(float32(maxLevel-originalRateClass.AlertLevel)*pct)
- rateClass.AlertLevel = clamp(newLimitLevel, originalRateClass.AlertLevel, originalRateClass.MaxLevel)
- s.warningCh <- s.warning
- return true, s.warning
- }
- // SubscribeRateLimits subscribes to rate limit updates.
- func (s *Session) SubscribeRateLimits(classes []wire.RateLimitClassID) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- for _, classID := range classes {
- s.rateLimitStates[classID-1].Subscribed = true
- }
- }
- // Warning returns the user's current warning level.
- func (s *Session) Warning() uint16 {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.warning
- }
- // WarningCh returns the warning notification channel.
- func (s *Session) WarningCh() chan uint16 {
- return s.warningCh
- }
- //
- // Lifecycle
- //
- // CloseSession closes all instances in the session.
- func (s *Session) CloseSession() {
- s.mutex.RLock()
- instances := make([]*SessionInstance, 0, len(s.instances))
- for _, instance := range s.instances {
- instances = append(instances, instance)
- }
- s.mutex.RUnlock()
- for _, instance := range instances {
- instance.closeOnly()
- }
- }
- // OnSessionClose registers a function to be called once all instances have closed.
- func (s *Session) OnSessionClose(fn func()) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.onSessCloseFn = fn
- }
- // RunOnce executes the given function once across all invocations. Used to
- // run arbitrary code that must only run once when the first session instance
- // connects. The function must not block.
- func (s *Session) RunOnce(fn func() error) error {
- var err error
- s.initOnce.Do(func() {
- err = fn()
- })
- return err
- }
- // SetNowFn sets the function used to get the current time. This is useful for testing.
- func (s *Session) SetNowFn(fn func() time.Time) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.nowFn = fn
- }
- //
- // User Settings / Attributes
- //
- // AllUserInfoBitmask returns whether all instances have user info flag set.
- func (s *Session) AllUserInfoBitmask(flag uint16) bool {
- for _, instance := range s.Instances() {
- if instance.UserInfoBitmask()&flag != flag {
- return false
- }
- }
- return true
- }
- // AllUserStatusBitmask returns whether all instances have user status flag set.
- func (s *Session) AllUserStatusBitmask(flag uint32) bool {
- for _, instance := range s.Instances() {
- if instance.UserStatusBitmask()&flag != flag {
- return false
- }
- }
- return true
- }
- // BuddyIcon returns the session's buddy icon metadata and reports whether it
- // has been set.
- func (s *Session) BuddyIcon() (wire.BARTID, bool) {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- icon := s.buddyIcon
- return icon, icon.Type != 0
- }
- // Caps returns the union of all capability UUIDs from all instances in the session.
- func (s *Session) Caps() [][16]byte {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- caps := make(map[[16]byte]bool)
- for _, instance := range s.instances {
- for _, c := range instance.caps() {
- caps[c] = true
- }
- }
- ret := make([][16]byte, 0, len(caps))
- for c := range caps {
- ret = append(ret, c)
- }
- // Sort capabilities to ensure deterministic order
- slices.SortFunc(ret, func(a, b [16]byte) int {
- for i := 0; i < 16; i++ {
- if a[i] != b[i] {
- return int(a[i]) - int(b[i])
- }
- }
- return 0
- })
- return ret
- }
- // HasCap returns true if any instance in the session has the given capability UUID.
- func (s *Session) HasCap(cap [16]byte) bool {
- for _, c := range s.Caps() {
- if c == cap {
- return true
- }
- }
- return false
- }
- // MemberSince reports when the user became a member.
- func (s *Session) MemberSince() time.Time {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.memberSince
- }
- // OfflineMsgCount returns the offline message count.
- func (s *Session) OfflineMsgCount() int {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.offlineMsgCount
- }
- // Profile returns the most recently updated non-empty profile from all instances.
- func (s *Session) Profile() UserProfile {
- var latest UserProfile
- for _, instance := range s.Instances() {
- profile := instance.Profile()
- if profile.IsEmpty() {
- continue
- }
- if latest.IsEmpty() || profile.UpdateTime.After(latest.UpdateTime) {
- latest = profile
- }
- }
- return latest
- }
- // SetBuddyIcon stores the session's buddy icon metadata.
- func (s *Session) SetBuddyIcon(icon wire.BARTID) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.buddyIcon = icon
- }
- // SetChatRoomCookie sets the chat room cookie.
- func (s *Session) SetChatRoomCookie(cookie string) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.chatRoomCookie = cookie
- }
- // SetMemberSince sets the member since timestamp.
- func (s *Session) SetMemberSince(t time.Time) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.memberSince = t
- }
- // SetOfflineMsgCount sets the offline message count.
- func (s *Session) SetOfflineMsgCount(count int) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.offlineMsgCount = count
- }
- // SetSignonTime sets the session's sign-on time.
- func (s *Session) SetSignonTime(t time.Time) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.signonTime = t
- }
- // SetTypingEventsEnabled sets whether the session wants to send and receive typing events.
- func (s *Session) SetTypingEventsEnabled(enabled bool) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.typingEventsEnabled = enabled
- }
- // SignonTime returns the session's sign-on time.
- func (s *Session) SignonTime() time.Time {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.signonTime
- }
- // TLVUserInfo returns a TLV list containing session information aggregated from all instances.
- func (s *Session) TLVUserInfo() wire.TLVUserInfo {
- return wire.TLVUserInfo{
- ScreenName: s.DisplayScreenName().String(),
- WarningLevel: s.Warning(),
- TLVBlock: wire.TLVBlock{
- TLVList: s.userInfo(),
- },
- }
- }
- // TypingEventsEnabled indicates whether the session wants to send and receive typing events.
- func (s *Session) TypingEventsEnabled() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.typingEventsEnabled
- }
- // SetUsesFeedbag records that this session uses the feedbag (server-side) buddy list for this sign-on.
- func (s *Session) SetUsesFeedbag() {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.usesFeedbag = true
- }
- // UsesFeedbag reports whether the client completed the feedbag Use handshake this sign-on.
- func (s *Session) UsesFeedbag() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.usesFeedbag
- }
- func (s *Session) userInfo() wire.TLVList {
- tlvs := wire.TLVList{}
- // sign-in timestamp
- tlvs.Append(wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(s.SignonTime().Unix())))
- instances := s.Instances()
- // Use the first instance as a template for user flags. Most flags are static
- // and should be consistent across all instances; only the "away" flag may vary.
- // If instances differ in protocol type (ICQ vs AIM), that indicates an error.
- var baseUserFlags uint16
- if len(instances) > 0 {
- baseUserFlags = instances[0].UserInfoBitmask()
- }
- if s.Away() {
- baseUserFlags |= wire.OServiceUserFlagUnavailable
- } else {
- baseUserFlags &^= wire.OServiceUserFlagUnavailable
- }
- tlvs.Append(wire.NewTLVBE(wire.OServiceUserInfoUserFlags, baseUserFlags))
- // user status flags - user-level (shared)
- var statusBitmask uint32
- if len(instances) > 0 {
- statusBitmask = instances[0].UserStatusBitmask()
- for _, instance := range instances {
- statusBitmask &= instance.UserStatusBitmask()
- }
- }
- tlvs.Append(wire.NewTLVBE(wire.OServiceUserInfoStatus, statusBitmask))
- // idle status - use most recent idle time if all instances are idle
- if s.Idle() {
- mostRecentIdleTime := s.mostRecentIdleTime()
- tlvs.Append(wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(s.nowFn().Sub(mostRecentIdleTime).Minutes())))
- }
- // set buddy icon metadata, if user has buddy icon
- if icon, hasIcon := s.BuddyIcon(); hasIcon {
- tlvs.Append(wire.NewTLVBE(wire.OServiceUserInfoBARTInfo, icon))
- }
- // ICQ direct-connect info. The TLV is required for buddy arrival events to
- // work in ICQ, even if the values are set to default.
- if baseUserFlags&wire.OServiceUserFlagICQ == wire.OServiceUserFlagICQ {
- if len(instances) > 0 {
- tlvs.Append(wire.NewTLVBE(wire.OServiceUserInfoICQDC, instances[0].ICQDCInfo()))
- }
- }
- caps := s.Caps()
- if len(caps) > 0 {
- tlvs.Append(wire.NewTLVBE(wire.OServiceUserInfoOscarCaps, caps))
- }
- tlvs.Append(wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)))
- return tlvs
- }
- // SessionInstance represents a single client connection instance within a user's
- // session. Multiple SessionInstance objects can belong to the same Session,
- // allowing a user to maintain concurrent connections from different clients or
- // devices.
- //
- // SessionInstance stores connection-specific state such as the remote address,
- // sign-on completion status, client capabilities, idle state, and per-connection
- // profile data. It holds a reference to its parent Session to access shared
- // user-level data like identity, warning levels, and rate limiting state.
- //
- // All methods on SessionInstance are safe for concurrent use.
- type SessionInstance struct {
- session *Session
- mutex sync.RWMutex
- // Unique instance identifier
- instanceNum uint8
- // Per-session connection state
- remoteAddr *netip.AddrPort
- signonComplete bool
- closed bool
- stopCh chan struct{}
- msgCh chan wire.SNACMessage
- kerberosAuth bool
- // Per-session client information
- clientID string
- capabilities [][16]byte
- foodGroupVersions [wire.MDir + 1]uint16
- multiConnFlag wire.MultiConnFlag
- toc2 bool
- toc2MsgEnc bool
- icqDCInfo wire.ICQDCInfo
- // Per-session state
- idle bool
- idleTime time.Time
- awayMsg string
- userInfoBitmask uint16
- userStatusBitmask uint32
- // contactsInit indicates whether the client-side buddy list or feedbag has been initialized
- contactsInit bool
- // notify transaction: accumulate visibility notification targets during feedbag batches
- notifyTxnActive bool
- notifyTxnShouldSend bool
- notifyTxnNames map[IdentScreenName]struct{}
- // Per-session profile
- profile UserProfile
- awayTime time.Time
- onInstanceCloseFn func()
- }
- // Session returns the parent Session for this instance.
- func (s *SessionInstance) Session() *Session {
- return s.session
- }
- //
- // Identity
- //
- // ChatRoomCookie returns the chat room cookie from the parent session.
- func (s *SessionInstance) ChatRoomCookie() string {
- return s.session.ChatRoomCookie()
- }
- // ClientID retrieves the instance's client ID.
- func (s *SessionInstance) ClientID() string {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.clientID
- }
- // DisplayScreenName returns the user's display screen name.
- func (s *SessionInstance) DisplayScreenName() DisplayScreenName {
- return s.session.DisplayScreenName()
- }
- // IdentScreenName returns the user's identity screen name.
- func (s *SessionInstance) IdentScreenName() IdentScreenName {
- return s.session.IdentScreenName()
- }
- // Num returns the unique instance identifier.
- func (s *SessionInstance) Num() uint8 {
- return s.instanceNum
- }
- // UIN returns the user's ICQ number.
- func (s *SessionInstance) UIN() uint32 {
- return s.session.UIN()
- }
- // SetClientID sets the instance's client ID.
- func (s *SessionInstance) SetClientID(clientID string) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.clientID = clientID
- }
- // SetTOC2 sets this instance to TOC2. supportsTOC2MsgEnc is true for toc2_login (encoded messaging), false for toc2_signon.
- func (s *SessionInstance) SetTOC2(supportsTOC2MsgEnc bool) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.toc2 = true
- s.toc2MsgEnc = supportsTOC2MsgEnc
- }
- // IsTOC2 returns true when the client is TOC2 (with or without encoded messaging).
- func (s *SessionInstance) IsTOC2() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.toc2
- }
- // SupportsTOC2MsgEnc returns true only when TOC2 with encoded messaging (toc2_login).
- func (s *SessionInstance) SupportsTOC2MsgEnc() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.toc2MsgEnc
- }
- //
- // Status / Availability
- //
- // Away returns true if the instance is away.
- func (s *SessionInstance) Away() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.away()
- }
- // AwayMessage returns the instance's away message and the time it was set.
- func (s *SessionInstance) AwayMessage() (string, time.Time) {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.awayMsg, s.awayTime
- }
- // Idle reports the instance's idle state.
- func (s *SessionInstance) Idle() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.idle
- }
- // IdleTime reports when the instance went idle.
- func (s *SessionInstance) IdleTime() time.Time {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.idleTime
- }
- // Invisible returns true if the user is invisible.
- func (s *SessionInstance) Invisible() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.userStatusBitmask&wire.OServiceUserStatusInvisible == wire.OServiceUserStatusInvisible
- }
- // SetIdle sets the instance's idle state.
- func (s *SessionInstance) SetIdle(dur time.Duration) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.idle = true
- // set the time the instance became idle
- s.idleTime = s.session.nowFn().Add(-dur)
- }
- // SetSignonComplete indicates that the instance has completed the sign-on sequence.
- func (s *SessionInstance) SetSignonComplete() {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.signonComplete = true
- }
- // SignonComplete indicates whether the instance has completed the sign-on sequence.
- func (s *SessionInstance) SignonComplete() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.signonComplete
- }
- // UnsetIdle removes the instance's idle state.
- func (s *SessionInstance) UnsetIdle() {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.idle = false
- }
- // active returns true if the instance is active. An instance is considered active if:
- // - it is not closed
- // - it has completed the sign-on sequence
- // - it is not idle
- // - it is not away
- func (s *SessionInstance) active() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return !s.closed && s.signonComplete && !s.idle && !s.away()
- }
- // away checks if the instance is away based on bitmask flags.
- // This method must be called while holding the mutex lock.
- func (s *SessionInstance) away() bool {
- return s.userInfoBitmask&wire.OServiceUserFlagUnavailable != 0 ||
- s.userStatusBitmask&wire.OServiceUserStatusAway != 0
- }
- // live returns whether the instance is ready to receive messages.
- func (s *SessionInstance) live() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return !s.closed && s.signonComplete
- }
- //
- // Rate Limiting / Warning
- //
- // RateLimitStates returns the current rate limit states.
- func (s *SessionInstance) RateLimitStates() [5]RateClassState {
- return s.session.RateLimitStates()
- }
- // Warning returns the user's current warning level.
- func (s *SessionInstance) Warning() uint16 {
- return s.session.Warning()
- }
- // WarningCh returns the warning notification channel.
- func (s *SessionInstance) WarningCh() chan uint16 {
- return s.session.WarningCh()
- }
- //
- // Lifecycle
- //
- // Closed blocks until the instance is closed.
- func (s *SessionInstance) Closed() <-chan struct{} {
- return s.stopCh
- }
- // CloseInstance shuts down the instance's ability to relay messages and removes it from the session.
- func (s *SessionInstance) CloseInstance() {
- s.mutex.Lock()
- if s.closed {
- s.mutex.Unlock()
- return
- }
- close(s.stopCh)
- s.closed = true
- onInstanceCloseFn := s.onInstanceCloseFn
- s.mutex.Unlock()
- // remove the instance now so that the function has an updated view of the world
- s.session.RemoveInstance(s)
- count := s.session.InstanceCount()
- if count == 0 {
- s.session.mutex.RLock()
- onSessCloseFn := s.session.onSessCloseFn
- s.session.mutex.RUnlock()
- onSessCloseFn()
- } else {
- onInstanceCloseFn()
- }
- }
- // OnClose registers a function to be called when the instance closes,
- // but only if other instances remain in the session. If this is the last instance
- // to close, OnSessionClose will be called instead.
- func (s *SessionInstance) OnClose(fn func()) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.onInstanceCloseFn = fn
- }
- // ContactsInit returns whether the client-side buddy list has been loaded or
- // the feedbag has been initialized.
- func (s *SessionInstance) ContactsInit() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.contactsInit
- }
- // SetContactsInit indicates that the client-side buddy list has been loaded or
- // the feedbag has been initialized.
- func (s *SessionInstance) SetContactsInit() {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.contactsInit = true
- }
- // BeginNotifyTxn starts accumulating notification targets during feedbag transactions.
- func (s *SessionInstance) BeginNotifyTxn() {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.notifyTxnShouldSend = false
- s.notifyTxnNames = nil
- s.notifyTxnActive = true
- }
- // InNotifyTxn reports whether a notification transaction is open.
- func (s *SessionInstance) InNotifyTxn() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.notifyTxnActive
- }
- // NotifyTxn records that a visibility notification should be sent when the transaction
- // ends, optionally limiting it to the given ident screen names. If no names are provided,
- // all buddies will be notified.
- func (s *SessionInstance) NotifyTxn(names ...IdentScreenName) error {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- if !s.notifyTxnActive {
- return fmt.Errorf("notify transaction: %w", errNotifyTxnNotActive)
- }
- if len(s.notifyTxnNames)+len(names) > maxNotifyTxnNames {
- return fmt.Errorf("notify transaction: %w", errNotifyTxnTooManyNames)
- }
- s.notifyTxnShouldSend = true
- if s.notifyTxnNames == nil {
- s.notifyTxnNames = make(map[IdentScreenName]struct{})
- }
- for _, name := range names {
- s.notifyTxnNames[name] = struct{}{}
- }
- return nil
- }
- // EndNotifyTxn returns whether to send a notification and accumulated notification targets,
- // then clears transaction state.
- func (s *SessionInstance) EndNotifyTxn() (shouldNotify bool, screenNames []IdentScreenName) {
- s.mutex.Lock()
- defer func() {
- s.notifyTxnActive = false
- s.notifyTxnShouldSend = false
- s.notifyTxnNames = nil
- s.mutex.Unlock()
- }()
- if !s.notifyTxnActive {
- return false, nil
- }
- shouldNotify = s.notifyTxnShouldSend
- if len(s.notifyTxnNames) > 0 {
- screenNames = make([]IdentScreenName, 0, len(s.notifyTxnNames))
- for name := range s.notifyTxnNames {
- screenNames = append(screenNames, name)
- }
- }
- return shouldNotify, screenNames
- }
- // CloseInstance shuts down the instance's ability to relay messages.
- func (s *SessionInstance) closeOnly() {
- s.mutex.Lock()
- if s.closed {
- s.mutex.Unlock()
- return
- }
- close(s.stopCh)
- s.closed = true
- s.mutex.Unlock()
- s.session.RemoveInstance(s)
- if s.session.InstanceCount() == 0 {
- s.session.mutex.RLock()
- onSessCloseFn := s.session.onSessCloseFn
- s.session.mutex.RUnlock()
- onSessCloseFn()
- }
- }
- //
- // User Settings / Attributes
- //
- // ClearUserInfoFlag clears a flag from the user info bitmask.
- func (s *SessionInstance) ClearUserInfoFlag(flag uint16) (flags uint16) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.userInfoBitmask &^= flag
- return s.userInfoBitmask
- }
- // FoodGroupVersions retrieves the instance's supported food group versions.
- func (s *SessionInstance) FoodGroupVersions() [wire.MDir + 1]uint16 {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.foodGroupVersions
- }
- // KerberosAuth indicates whether Kerberos authentication was used for this instance.
- func (s *SessionInstance) KerberosAuth() bool {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.kerberosAuth
- }
- // MultiConnFlag retrieves the multi-connection flag for this instance.
- func (s *SessionInstance) MultiConnFlag() wire.MultiConnFlag {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.multiConnFlag
- }
- // OfflineMsgCount returns the offline message count.
- func (s *SessionInstance) OfflineMsgCount() int {
- return s.session.OfflineMsgCount()
- }
- // Profile returns the user's profile information.
- func (s *SessionInstance) Profile() UserProfile {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.profile
- }
- // RemoteAddr returns the instance's remote IP address.
- func (s *SessionInstance) RemoteAddr() (remoteAddr *netip.AddrPort) {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.remoteAddr
- }
- // SetAwayMessage sets the instance's away message.
- func (s *SessionInstance) SetAwayMessage(awayMessage string) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.awayMsg = awayMessage
- }
- // SetCaps sets capability UUIDs for the instance.
- func (s *SessionInstance) SetCaps(caps [][16]byte) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.capabilities = caps
- }
- // SetFoodGroupVersions sets the instance's supported food group versions.
- func (s *SessionInstance) SetFoodGroupVersions(versions [wire.MDir + 1]uint16) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.foodGroupVersions = versions
- }
- // SetKerberosAuth sets whether Kerberos authentication was used for this instance.
- func (s *SessionInstance) SetKerberosAuth(enabled bool) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.kerberosAuth = enabled
- }
- // SetMultiConnFlag sets the multi-connection flag for this instance.
- func (s *SessionInstance) SetMultiConnFlag(flag wire.MultiConnFlag) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.multiConnFlag = flag
- }
- // SetProfile sets the user's profile information.
- func (s *SessionInstance) SetProfile(profile UserProfile) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.profile = profile
- }
- // SetRemoteAddr sets the instance's remote IP address.
- func (s *SessionInstance) SetRemoteAddr(remoteAddr *netip.AddrPort) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.remoteAddr = remoteAddr
- }
- // SetUserInfoFlag sets a flag on the user info bitmask.
- func (s *SessionInstance) SetUserInfoFlag(flag uint16) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- if flag == wire.OServiceUserFlagUnavailable {
- s.awayTime = s.session.nowFn()
- }
- s.userInfoBitmask |= flag
- }
- // SetUserStatusBitmask sets the user status bitmask.
- func (s *SessionInstance) SetUserStatusBitmask(bitmask uint32) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- if bitmask&wire.OServiceUserStatusAway == wire.OServiceUserStatusAway {
- if !s.away() {
- s.awayTime = s.session.nowFn()
- }
- }
- s.userStatusBitmask = bitmask
- }
- // SignonTime returns the session's sign-on time.
- func (s *SessionInstance) SignonTime() time.Time {
- return s.session.SignonTime()
- }
- // TypingEventsEnabled indicates whether the session wants to send and receive typing events.
- func (s *SessionInstance) TypingEventsEnabled() bool {
- return s.session.TypingEventsEnabled()
- }
- // UserInfoBitmask returns the user info bitmask.
- func (s *SessionInstance) UserInfoBitmask() uint16 {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.userInfoBitmask
- }
- // UserStatusBitmask returns the user status bitmask.
- func (s *SessionInstance) UserStatusBitmask() uint32 {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.userStatusBitmask
- }
- // SetICQDCInfo stores ICQ direct-connect settings from TLV 0x0C on SNAC(01,1E).
- func (s *SessionInstance) SetICQDCInfo(info wire.ICQDCInfo) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.icqDCInfo = info
- }
- // ICQDCInfo returns stored ICQ direct-connect settings.
- func (s *SessionInstance) ICQDCInfo() wire.ICQDCInfo {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.icqDCInfo
- }
- // caps retrieves instance capabilities.
- func (s *SessionInstance) caps() [][16]byte {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- return s.capabilities
- }
- //
- // Message Sending
- //
- // ReceiveMessage returns a channel of messages relayed via this instance.
- func (s *SessionInstance) ReceiveMessage() chan wire.SNACMessage {
- return s.msgCh
- }
- // RelayMessageToInstance receives a SNAC message and passes it to the instance's message channel.
- func (s *SessionInstance) RelayMessageToInstance(msg wire.SNACMessage) SessSendStatus {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- if s.closed {
- return SessSendClosed
- }
- select {
- case s.msgCh <- msg:
- return SessSendOK
- case <-s.stopCh:
- return SessSendClosed
- default:
- return SessQueueFull
- }
- }
|