]> git.xonotic.org Git - xonotic/xonotic.git/blob - misc/infrastructure/powerbot/powerlevels.go
Make ./all clean --reclone work with gmqcc repo.
[xonotic/xonotic.git] / misc / infrastructure / powerbot / powerlevels.go
1 package main
2
3 import (
4         "github.com/google/go-cmp/cmp"
5         "log"
6         "math"
7         "maunium.net/go/mautrix"
8         "maunium.net/go/mautrix/event"
9         "maunium.net/go/mautrix/id"
10         "time"
11 )
12
13 const (
14         idleScore   = 1
15         activeScore = 100
16         activeTime  = 5 * time.Minute
17         // 15 minutes idling = PL 1.
18         minPowerScore = 15 * 60 * idleScore
19         minPowerLevel = 1
20         // 1 year fulltime active dev = PL 10.
21         maxPowerScore = 3600 * (365*24*idleScore + 8*261*(activeScore-idleScore))
22         maxPowerLevel = 9
23         // Do not touch users outside this range.
24         minApplyLevel = 0
25         maxApplyLevel = 9
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
30 )
31
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).",
36                         i, score,
37                         time.Duration(float64(time.Second)*score/idleScore),
38                         time.Duration(float64(time.Second)*score/activeScore),
39                 )
40         }
41 }
42
43 func computePowerLevel(def int, score Score) (int, float64) {
44         points := score.Idle.Seconds()*idleScore + score.Active.Seconds()*activeScore
45         if points <= 0 {
46                 return def, math.Inf(-1)
47         }
48         raw := minPowerLevel + (maxPowerLevel-minPowerLevel)*math.Log(points/minPowerScore)/math.Log(maxPowerScore/minPowerScore)
49         if raw < minPowerLevel {
50                 return def, raw
51         }
52         if points > maxPowerScore {
53                 return maxPowerLevel, raw
54         }
55         return int(math.Floor(raw)), raw
56 }
57
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)
62         }
63         ret = append(ret, roomLevels.EventsDefault)
64         if roomLevels.InvitePtr != nil {
65                 ret = append(ret, *roomLevels.InvitePtr)
66         }
67         if roomLevels.KickPtr != nil {
68                 ret = append(ret, *roomLevels.KickPtr)
69         }
70         if roomLevels.BanPtr != nil {
71                 ret = append(ret, *roomLevels.BanPtr)
72         }
73         if roomLevels.RedactPtr != nil {
74                 ret = append(ret, *roomLevels.RedactPtr)
75         }
76         return ret
77 }
78
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)
83                 return
84         }
85         tryUpdate := force
86         for _, level := range allPowerLevels(roomLevels) {
87                 if minPowerLevel <= level && level <= maxPowerLevel {
88                         tryUpdate = true
89                 }
90         }
91         if !tryUpdate {
92                 log.Printf("room %v skipping because PLs currently do not matter", room)
93                 return
94         }
95         log.Printf("room %v considering to update PLs", room)
96         if fullySynced {
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
104                         }
105                 }
106         }
107         newRoomLevels := *roomLevels
108         newRoomLevels.Users = make(map[id.UserID]int)
109         for user, level := range roomLevels.Users {
110                 if level == roomLevels.UsersDefault {
111                         continue
112                 }
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)
119                         continue
120                 }
121                 newRoomLevels.Users[user] = level
122         }
123         dirty := false
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.
128                         continue
129                 }
130                 prevLevel := roomLevels.Users[user]
131                 level, raw := computePowerLevel(roomLevels.UsersDefault, *score)
132                 for _, otherRoom := range roomGroup {
133                         if otherRoom == room {
134                                 continue
135                         }
136                         otherScore := scores[otherRoom][user]
137                         if otherScore == nil {
138                                 continue
139                         }
140                         otherLevel, otherRaw := computePowerLevel(roomLevels.UsersDefault, *otherScore)
141                         if otherLevel > level {
142                                 level = otherLevel
143                         }
144                         if otherRaw > raw {
145                                 raw = otherRaw
146                         }
147                 }
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
157                         dirty = true
158                 } else {
159                         log.Printf("room %v user %v power level: KEEP %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
160                 }
161         }
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)
168                                 dirty = true
169                         }
170                 }
171                 clearPowerLevel++
172         }
173         if dirty {
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)
177                 if err != nil {
178                         log.Printf("Failed to update power levels: %v", err)
179                 }
180         } else {
181                 log.Printf("room %v nothing to update", room)
182         }
183 }