dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

managers.go (8067B)


      1 package managers
      2 
      3 import (
      4 	"dkforest/pkg/hashset"
      5 	"encoding/json"
      6 	"fmt"
      7 	"sort"
      8 	"strings"
      9 	"sync"
     10 	"time"
     11 
     12 	"dkforest/pkg/database"
     13 
     14 	"dkforest/pkg/utils"
     15 )
     16 
     17 func init() {
     18 	ActiveUsers = NewActiveUsersManager()
     19 }
     20 
     21 type UserInfo struct {
     22 	UserID              database.UserID
     23 	Username            database.Username
     24 	Color               string
     25 	RefreshRate         int64
     26 	LastUpdate          time.Time
     27 	LastActivity        *time.Time
     28 	IsModerator         bool
     29 	IsIncognito         bool
     30 	IsHellbanned        bool
     31 	AfkIndicatorEnabled bool
     32 }
     33 
     34 type IUserInfoUser interface {
     35 	GetID() database.UserID
     36 	GetUsername() database.Username
     37 	GetRefreshRate() int64
     38 	GetChatColor() string
     39 	IsModerator() bool
     40 	GetIsIncognito() bool
     41 	GetIsHellbanned() bool
     42 	GetAFK() bool
     43 	GetAfkIndicatorEnabled() bool
     44 }
     45 
     46 func newUserInfo(user IUserInfoUser, lastActivity *time.Time) UserInfo {
     47 	return UserInfo{
     48 		UserID:              user.GetID(),
     49 		Username:            user.GetUsername(),
     50 		RefreshRate:         user.GetRefreshRate(),
     51 		Color:               user.GetChatColor(),
     52 		IsModerator:         user.IsModerator(),
     53 		IsIncognito:         user.GetIsIncognito(),
     54 		IsHellbanned:        user.GetIsHellbanned(),
     55 		AfkIndicatorEnabled: user.GetAFK() && user.GetAfkIndicatorEnabled(),
     56 		LastUpdate:          time.Now(),
     57 		LastActivity:        lastActivity,
     58 	}
     59 }
     60 
     61 func NewUserInfo(user IUserInfoUser) UserInfo {
     62 	return newUserInfo(user, nil)
     63 }
     64 
     65 func NewUserInfoUpdateActivity(user IUserInfoUser) UserInfo {
     66 	now := time.Now()
     67 	return newUserInfo(user, &now)
     68 }
     69 
     70 func (m UserInfo) MarshalJSON() ([]byte, error) {
     71 	return json.Marshal(&struct {
     72 		Username database.Username
     73 		Color    string
     74 	}{
     75 		Username: m.Username,
     76 		Color:    m.Color,
     77 	})
     78 }
     79 
     80 type UsersMap map[database.Username]UserInfo // Username -> UserInfo
     81 
     82 func (m UsersMap) ToArray() []UserInfo {
     83 	out := make([]UserInfo, len(m))
     84 	i := 0
     85 	for _, userInfo := range m {
     86 		out[i] = userInfo
     87 		i++
     88 	}
     89 	sort.Slice(out, func(i, j int) bool {
     90 		if out[i].LastActivity != nil && out[j].LastActivity != nil {
     91 			if out[i].LastActivity.After(*out[j].LastActivity) {
     92 				return true
     93 			} else if out[i].LastActivity.Before(*out[j].LastActivity) {
     94 				return false
     95 			}
     96 		}
     97 		return out[i].Username < out[j].Username
     98 	})
     99 	return out
    100 }
    101 
    102 const privateRoomKeyPrefix = "p_"
    103 
    104 type RoomKey string
    105 
    106 func (r RoomKey) isPrivateRoom() bool {
    107 	return strings.HasPrefix(string(r), privateRoomKeyPrefix)
    108 }
    109 
    110 func getRoomKey(room database.ChatRoom) RoomKey {
    111 	if room.IsProtected() {
    112 		return RoomKey(fmt.Sprintf("%s%d", privateRoomKeyPrefix, room.ID))
    113 	}
    114 	return RoomKey(utils.FormatInt64(int64(room.ID)))
    115 }
    116 
    117 type ActiveUsersManager struct {
    118 	sync.RWMutex
    119 	activeUsers map[RoomKey]UsersMap
    120 }
    121 
    122 func NewActiveUsersManager() *ActiveUsersManager {
    123 	m := new(ActiveUsersManager)
    124 	m.activeUsers = make(map[RoomKey]UsersMap)
    125 	return m
    126 }
    127 
    128 var ActiveUsers *ActiveUsersManager
    129 
    130 func (m *ActiveUsersManager) UpdateUserInRoom(room database.ChatRoom, userInfo UserInfo) {
    131 	if userInfo.IsIncognito {
    132 		return
    133 	}
    134 	roomKey := getRoomKey(room)
    135 	usersMap := m.getRoomUsersMap(roomKey)
    136 
    137 	m.RLock()
    138 	prevUserInfo := usersMap[userInfo.Username]
    139 	m.RUnlock()
    140 	if prevUserInfo.LastActivity == nil && userInfo.LastActivity == nil {
    141 		now := time.Now()
    142 		userInfo.LastActivity = &now
    143 	} else if userInfo.LastActivity == nil {
    144 		userInfo.LastActivity = prevUserInfo.LastActivity
    145 	}
    146 
    147 	m.Lock()
    148 	usersMap[userInfo.Username] = userInfo
    149 	m.activeUsers[roomKey] = usersMap
    150 	m.Unlock()
    151 }
    152 
    153 // UpdateUserHBInRooms Update the IsHellbanned property for a user.
    154 // This is needed to ensure the user become invisible at the same time as his messages disappears.
    155 // Otherwise, it is possible that the message becomes HB and the user still show up in the users list.
    156 func (m *ActiveUsersManager) UpdateUserHBInRooms(newUserInfo UserInfo) {
    157 	m.Lock()
    158 	for roomKey, usersMap := range m.activeUsers {
    159 		for username, userInfo := range usersMap {
    160 			if userInfo.UserID == newUserInfo.UserID {
    161 				prevUserInfo := m.activeUsers[roomKey][username]
    162 				prevUserInfo.IsHellbanned = newUserInfo.IsHellbanned
    163 				m.activeUsers[roomKey][username] = prevUserInfo
    164 			}
    165 		}
    166 	}
    167 	m.Unlock()
    168 }
    169 
    170 func (m *ActiveUsersManager) getRoomUsersMap(roomKey RoomKey) UsersMap {
    171 	emptyUsersMap := make(UsersMap)
    172 	m.RLock()
    173 	usersMap, found := m.activeUsers[roomKey]
    174 	m.RUnlock()
    175 	if !found {
    176 		m.Lock()
    177 		m.activeUsers[roomKey] = emptyUsersMap
    178 		usersMap = emptyUsersMap
    179 		m.Unlock()
    180 	}
    181 	return usersMap
    182 }
    183 
    184 func (m *ActiveUsersManager) LocateUser(target database.Username) (out []database.RoomID) {
    185 	m.RLock()
    186 	for roomKey, usersMap := range m.activeUsers {
    187 		for username := range usersMap {
    188 			if strings.ToLower(string(username)) == strings.ToLower(string(target)) {
    189 				roomID := database.RoomID(utils.DoParseInt64(string(roomKey)))
    190 				out = append(out, roomID)
    191 			}
    192 		}
    193 	}
    194 	m.RUnlock()
    195 	return
    196 }
    197 
    198 // GetActiveUsers gets a list of all users that are in public rooms.
    199 // We use this to display online users on the home (login) page if the feature is enabled.
    200 func (m *ActiveUsersManager) GetActiveUsers() []UserInfo {
    201 	activeUsers := make(UsersMap)
    202 	m.RLock()
    203 	defer m.RUnlock()
    204 	for roomKey, usersMap := range m.activeUsers {
    205 		for username, userInfo := range usersMap {
    206 			if !roomKey.isPrivateRoom() { // Skip people who are in private rooms
    207 				activeUsers[username] = userInfo
    208 			}
    209 		}
    210 	}
    211 	return activeUsers.ToArray()
    212 }
    213 
    214 func GetUserIgnoreSet(db *database.DkfDB, authUser *database.User) *hashset.HashSet[database.Username] {
    215 	ignoredSet := hashset.New[database.Username]()
    216 	// Only fill the ignored set if the user does not display the ignored users ("Toggle ignored" chat setting)
    217 	// and if the user has "Hide ignored users from users lists" enabled (user setting)
    218 	if !authUser.DisplayIgnored && authUser.HideIgnoredUsersFromList {
    219 		ignoredUsersUsernames, _ := db.GetIgnoredUsersUsernames(authUser.ID)
    220 		for _, ignoredUserUsername := range ignoredUsersUsernames {
    221 			ignoredSet.Insert(ignoredUserUsername)
    222 		}
    223 	}
    224 	return ignoredSet
    225 }
    226 
    227 func (m *ActiveUsersManager) GetRoomUsers(room database.ChatRoom, ignoredSet *hashset.HashSet[database.Username]) (inRoom, inChat []UserInfo) {
    228 	outsideUsers := make(UsersMap)
    229 	newRoomUsersMap := make(UsersMap)
    230 	roomIDStr := getRoomKey(room)
    231 	// clone managers map into local variable map
    232 	m.RLock()
    233 	defer m.RUnlock()
    234 	if roomUsersMap, ok := m.activeUsers[roomIDStr]; ok {
    235 		for username, userInfo := range roomUsersMap {
    236 			newRoomUsersMap[username] = userInfo
    237 		}
    238 	}
    239 	// Build map of users outside of current room
    240 	for roomKey, usersMap := range m.activeUsers {
    241 		for username, userInfo := range usersMap {
    242 			if roomKey == roomIDStr || roomKey.isPrivateRoom() { // Skip people who are in private rooms
    243 				continue
    244 			}
    245 			// Only add users if they're not already in the current room
    246 			if _, ok := newRoomUsersMap[username]; !ok {
    247 				outsideUsers[username] = userInfo
    248 			}
    249 		}
    250 	}
    251 	// Delete ignored users
    252 	ignoredSet.Each(func(ignoreUsername database.Username) {
    253 		delete(newRoomUsersMap, ignoreUsername)
    254 		delete(outsideUsers, ignoreUsername)
    255 	})
    256 	inRoom = newRoomUsersMap.ToArray()
    257 	inChat = outsideUsers.ToArray()
    258 	return
    259 }
    260 
    261 // RemoveUser from active users
    262 func (m *ActiveUsersManager) RemoveUser(userID database.UserID) {
    263 	m.Lock()
    264 	defer m.Unlock()
    265 	for _, usersMap := range m.activeUsers {
    266 		for k, v := range usersMap {
    267 			if v.UserID == userID {
    268 				delete(usersMap, k)
    269 			}
    270 		}
    271 	}
    272 }
    273 
    274 func (m *ActiveUsersManager) IsUserActiveInRoom(userID database.UserID, room database.ChatRoom) (found bool) {
    275 	m.RLock()
    276 	defer m.RUnlock()
    277 	usersMap, found := m.activeUsers[getRoomKey(room)]
    278 	if !found {
    279 		return false
    280 	}
    281 	for _, v := range usersMap {
    282 		if v.UserID == userID {
    283 			return true
    284 		}
    285 	}
    286 	return false
    287 }
    288 
    289 func (m *ActiveUsersManager) CleanupUsersCache() {
    290 	for {
    291 		select {
    292 		case <-time.After(10 * time.Second):
    293 		}
    294 		m.Lock()
    295 		for _, usersMap := range m.activeUsers {
    296 			for k, userInfo := range usersMap {
    297 				if time.Since(userInfo.LastUpdate) > time.Duration(userInfo.RefreshRate+25)*time.Second {
    298 					delete(usersMap, k)
    299 				}
    300 			}
    301 		}
    302 		m.Unlock()
    303 	}
    304 }