4 "github.com/google/go-cmp/cmp"
7 "maunium.net/go/mautrix"
8 "maunium.net/go/mautrix/event"
9 "maunium.net/go/mautrix/id"
16 activeTime = 5 * time.Minute
17 // 15 minutes idling = PL 1.
18 minPowerScore = 15 * 60 * idleScore
20 // 1 year fulltime active dev = PL 10.
21 maxPowerScore = 3600 * (365*24*idleScore + 8*261*(activeScore-idleScore))
23 // Do not touch users outside this range.
26 // Expire power level if no event for 1 month. Level comes back on next event, including join.
27 powerExpireTime = time.Hour * 24 * 30
28 // Maximum count of ACL entries. Should avoid hitting the 64k limit.
29 maxPowerLevelEntries = 2048
32 func logPowerLevelBounds() {
33 for i := minPowerLevel; i <= maxPowerLevel; i++ {
34 score := minPowerScore * math.Pow(maxPowerScore/minPowerScore, float64(i-minPowerLevel)/float64(maxPowerLevel-minPowerLevel))
35 log.Printf("Power level %d requires score %v (= %v idle or %v active).",
37 time.Duration(float64(time.Second)*score/idleScore),
38 time.Duration(float64(time.Second)*score/activeScore),
43 func computePowerLevel(def int, score Score) (int, float64) {
44 points := score.Idle.Seconds()*idleScore + score.Active.Seconds()*activeScore
46 return def, math.Inf(-1)
48 raw := minPowerLevel + (maxPowerLevel-minPowerLevel)*math.Log(points/minPowerScore)/math.Log(maxPowerScore/minPowerScore)
49 if raw < minPowerLevel {
52 if points > maxPowerScore {
53 return maxPowerLevel, raw
55 return int(math.Floor(raw)), raw
58 func allPowerLevels(roomLevels *event.PowerLevelsEventContent) []int {
59 ret := make([]int, 0, len(roomLevels.Events)+5)
60 for _, level := range roomLevels.Events {
61 ret = append(ret, level)
63 ret = append(ret, roomLevels.EventsDefault)
64 if roomLevels.InvitePtr != nil {
65 ret = append(ret, *roomLevels.InvitePtr)
67 if roomLevels.KickPtr != nil {
68 ret = append(ret, *roomLevels.KickPtr)
70 if roomLevels.BanPtr != nil {
71 ret = append(ret, *roomLevels.BanPtr)
73 if roomLevels.RedactPtr != nil {
74 ret = append(ret, *roomLevels.RedactPtr)
79 func syncPowerLevels(client *mautrix.Client, room id.RoomID, roomGroup []id.RoomID, scores map[id.RoomID]map[id.UserID]*Score, force bool) {
80 roomLevels := roomPowerLevels[room]
81 if roomLevels == nil {
82 log.Printf("trying to ensure power levels for room %v, but did not get power level map yet", room)
86 for _, level := range allPowerLevels(roomLevels) {
87 if minPowerLevel <= level && level <= maxPowerLevel {
92 log.Printf("room %v skipping because PLs currently do not matter", room)
95 log.Printf("room %v considering to update PLs", room)
97 for user, score := range scores[room] {
98 // Expire users that for some reason did not get pruned from the database.
99 // This may cause them to lose their power level below.
100 if _, found := roomUsers[room][user]; !found && score.CurrentState != NotActive {
101 log.Printf("Pruning long inactive user %v from room %v.", user, room)
102 setUserStateAt(room, user, time.Now(), NotActive, NotActive)
103 score.CurrentState = NotActive
107 newRoomLevels := *roomLevels
108 newRoomLevels.Users = make(map[id.UserID]int)
109 for user, level := range roomLevels.Users {
110 if level == roomLevels.UsersDefault {
113 // TODO: Also skip users who aren't in the room for ages.
114 score := scores[room][user]
115 if level >= minPowerLevel && level <= maxPowerLevel && score.CurrentState == NotActive && time.Now().After(score.LastEvent.Add(powerExpireTime)) {
116 // User is inactive - prune them from the power level list. Saves space.
117 // But this doesn't mark the list dirty as there is no need to send an update.
118 log.Printf("room %v user %v power level: PRUNE %v (%v)", room, user, level, score)
121 newRoomLevels.Users[user] = level
124 log.Printf("room %v", room)
125 for user, score := range scores[room] {
126 if score.CurrentState == NotActive {
127 // Do not add/bump power levels for users not in the room.
130 prevLevel := roomLevels.Users[user]
131 level, raw := computePowerLevel(roomLevels.UsersDefault, *score)
132 for _, otherRoom := range roomGroup {
133 if otherRoom == room {
136 otherScore := scores[otherRoom][user]
137 if otherScore == nil {
140 otherLevel, otherRaw := computePowerLevel(roomLevels.UsersDefault, *otherScore)
141 if otherLevel > level {
148 if prevLevel < minApplyLevel {
149 log.Printf("room %v user %v power level: SKIP_TOO_LOW %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
150 } else if prevLevel > maxApplyLevel {
151 log.Printf("room %v user %v power level: SKIP_TOO_HIGH %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
152 } else if level < prevLevel {
153 log.Printf("room %v user %v power level: SKIP_WOULD_LOWER %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
154 } else if level > prevLevel {
155 log.Printf("room %v user %v power level: INCREASE %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
156 newRoomLevels.Users[user] = level
159 log.Printf("room %v user %v power level: KEEP %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
162 clearPowerLevel := minPowerLevel
163 for len(newRoomLevels.Users) > maxPowerLevelEntries && clearPowerLevel <= maxPowerLevel {
164 log.Printf("room %v not including power level %d to reduce message size", clearPowerLevel)
165 for user, level := range newRoomLevels.Users {
166 if level == clearPowerLevel {
167 delete(newRoomLevels.Users, user)
174 diff := cmp.Diff(roomLevels.Users, newRoomLevels.Users)
175 log.Printf("room %v power level update:\n%v", room, diff)
176 _, err := client.SendStateEvent(room, event.StatePowerLevels, "", newRoomLevels)
178 log.Printf("Failed to update power levels: %v", err)
181 log.Printf("room %v nothing to update", room)