]> git.xonotic.org Git - xonotic/xonotic.git/blob - misc/infrastructure/powerbot/powerlevels.go
7e480eb4f54b7fa60f099b885b992a9e230848db
[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         "reflect"
11         "time"
12 )
13
14 const (
15         idleScore   = 1
16         activeScore = 100
17         activeTime  = 5 * time.Minute
18         // 15 minutes idling = PL 1.
19         minPowerScore = 15 * 60 * idleScore
20         minPowerLevel = 1
21         // 1 year fulltime active dev = PL 10.
22         maxPowerScore = 3600 * (365*24*idleScore + 8*261*(activeScore-idleScore))
23         maxPowerLevel = 9
24         // Do not touch users outside this range.
25         minApplyLevel = 0
26         maxApplyLevel = 9
27         // Expire power level if no event for 1 month. Level comes back on next event, including join.
28         powerExpireTime = time.Hour * 24 * 30
29         // Maximum count of ACL entries. Should avoid hitting the 64k limit.
30         maxPowerLevelEntries = 2048
31 )
32
33 func logPowerLevelBounds() {
34         for i := minPowerLevel; i <= maxPowerLevel; i++ {
35                 score := minPowerScore * math.Pow(maxPowerScore/minPowerScore, float64(i-minPowerLevel)/float64(maxPowerLevel-minPowerLevel))
36                 log.Printf("Power level %d requires score %v (= %v idle or %v active).",
37                         i, score,
38                         time.Duration(float64(time.Second)*score/idleScore),
39                         time.Duration(float64(time.Second)*score/activeScore),
40                 )
41         }
42 }
43
44 func computePowerLevel(def int, score Score) (int, float64) {
45         points := score.Idle.Seconds()*idleScore + score.Active.Seconds()*activeScore
46         if points <= 0 {
47                 return def, math.Inf(-1)
48         }
49         raw := minPowerLevel + (maxPowerLevel-minPowerLevel)*math.Log(points/minPowerScore)/math.Log(maxPowerScore/minPowerScore)
50         if raw < minPowerLevel {
51                 return def, raw
52         }
53         if points > maxPowerScore {
54                 return maxPowerLevel, raw
55         }
56         return int(math.Floor(raw)), raw
57 }
58
59 func allPowerLevels(roomLevels *event.PowerLevelsEventContent) []int {
60         ret := make([]int, 0, len(roomLevels.Events)+5)
61         for _, level := range roomLevels.Events {
62                 ret = append(ret, level)
63         }
64         ret = append(ret, roomLevels.EventsDefault)
65         if roomLevels.InvitePtr != nil {
66                 ret = append(ret, *roomLevels.InvitePtr)
67         }
68         if roomLevels.KickPtr != nil {
69                 ret = append(ret, *roomLevels.KickPtr)
70         }
71         if roomLevels.BanPtr != nil {
72                 ret = append(ret, *roomLevels.BanPtr)
73         }
74         if roomLevels.RedactPtr != nil {
75                 ret = append(ret, *roomLevels.RedactPtr)
76         }
77         return ret
78 }
79
80 func syncPowerLevels(client *mautrix.Client, room id.RoomID, roomGroup []id.RoomID, scores map[id.RoomID]map[id.UserID]*Score, force bool) {
81         roomLevels := roomPowerLevels[room]
82         if roomLevels == nil {
83                 log.Printf("trying to ensure power levels for room %v, but did not get power level map yet", room)
84                 return
85         }
86         tryUpdate := force
87         for _, level := range allPowerLevels(roomLevels) {
88                 if minPowerLevel <= level && level <= maxPowerLevel {
89                         tryUpdate = true
90                 }
91         }
92         if !tryUpdate {
93                 log.Printf("room %v skipping because PLs currently do not matter", room)
94                 return
95         }
96         log.Printf("room %v considering to update PLs", room)
97         if fullySynced {
98                 for user, score := range scores[room] {
99                         // Expire users that for some reason did not get pruned from the database.
100                         // This may cause them to lose their power level below.
101                         if _, found := roomUsers[room][user]; !found && score.CurrentState != NotActive {
102                                 log.Printf("Pruning long inactive user %v from room %v.", user, room)
103                                 setUserStateAt(room, user, time.Now(), NotActive, NotActive)
104                                 score.CurrentState = NotActive
105                         }
106                 }
107         }
108         newRoomLevels := *roomLevels
109         newRoomLevels.Users = make(map[id.UserID]int)
110         for user, level := range roomLevels.Users {
111                 if level == roomLevels.UsersDefault {
112                         continue
113                 }
114                 // TODO: Also skip users who aren't in the room for ages.
115                 score := scores[room][user]
116                 if level >= minPowerLevel && level <= maxPowerLevel && score.CurrentState == NotActive && time.Now().After(score.LastEvent.Add(powerExpireTime)) {
117                         // User is inactive - prune them from the power level list. Saves space.
118                         // But this doesn't mark the list dirty as there is no need to send an update.
119                         log.Printf("room %v user %v power level: PRUNE %v (%v)", room, user, level, score)
120                         continue
121                 }
122                 newRoomLevels.Users[user] = level
123         }
124         dirty := false
125         log.Printf("room %v", room)
126         for user, score := range scores[room] {
127                 if score.CurrentState == NotActive {
128                         // Do not add/bump power levels for users not in the room.
129                         continue
130                 }
131                 prevLevel := roomLevels.Users[user]
132                 level, raw := computePowerLevel(roomLevels.UsersDefault, *score)
133                 for _, otherRoom := range roomGroup {
134                         if otherRoom == room {
135                                 continue
136                         }
137                         otherScore := scores[otherRoom][user]
138                         if otherScore == nil {
139                                 continue
140                         }
141                         otherLevel, otherRaw := computePowerLevel(roomLevels.UsersDefault, *otherScore)
142                         if otherLevel > level {
143                                 level = otherLevel
144                         }
145                         if otherRaw > raw {
146                                 raw = otherRaw
147                         }
148                 }
149                 if prevLevel < minApplyLevel {
150                         log.Printf("room %v user %v power level: SKIP_TOO_LOW %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
151                 } else if prevLevel > maxApplyLevel {
152                         log.Printf("room %v user %v power level: SKIP_TOO_HIGH %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
153                 } else if level < prevLevel {
154                         log.Printf("room %v user %v power level: SKIP_WOULD_LOWER %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
155                 } else if level > prevLevel {
156                         log.Printf("room %v user %v power level: INCREASE %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
157                         newRoomLevels.Users[user] = level
158                         dirty = true
159                 } else {
160                         log.Printf("room %v user %v power level: KEEP %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
161                 }
162         }
163         clearPowerLevel := minPowerLevel
164         for len(newRoomLevels.Users) > maxPowerLevelEntries && clearPowerLevel <= maxPowerLevel {
165                 log.Printf("room %v not including power level %d to reduce message size", clearPowerLevel)
166                 for user, level := range newRoomLevels.Users {
167                         if level == clearPowerLevel {
168                                 delete(newRoomLevels.Users, user)
169                                 dirty = true
170                         }
171                 }
172                 clearPowerLevel++
173         }
174         if dirty {
175                 diff := cmp.Diff(roomLevels.Users, newRoomLevels.Users)
176                 log.Printf("room %v power level update:\n%v", room, diff)
177                 _, err := client.SendStateEvent(room, event.StatePowerLevels, "", makeDefaultsExplicit(&newRoomLevels))
178                 if err != nil {
179                         log.Printf("Failed to update power levels: %v", err)
180                 }
181         } else {
182                 log.Printf("room %v nothing to update", room)
183         }
184 }
185
186 type powerLevelsWithDefaults struct {
187         // This struct is a copy of the public stuff in event.PowerLevelsEventContent,
188         // but with omitempty removed on users_default and events_default to work around
189         // https://github.com/matrix-org/dendrite/issues/2983
190         Users           map[id.UserID]int              `json:"users"`
191         UsersDefault    int                            `json:"users_default,omitempty"`
192         Events          map[string]int                 `json:"events"`
193         EventsDefault   int                            `json:"events_default,omitempty"`
194         Notifications   *event.NotificationPowerLevels `json:"notifications,omitempty"`
195         StateDefaultPtr *int                           `json:"state_default,omitempty"`
196         InvitePtr       *int                           `json:"invite,omitempty"`
197         KickPtr         *int                           `json:"kick,omitempty"`
198         BanPtr          *int                           `json:"ban,omitempty"`
199         RedactPtr       *int                           `json:"redact,omitempty"`
200         HistoricalPtr   *int                           `json:"historical,omitempty"`
201 }
202
203 func makeDefaultsExplicit(roomLevels *event.PowerLevelsEventContent) *powerLevelsWithDefaults {
204         // Copying over all exported fields using reflect.
205         // Doing it this way so if a new field is added to event.PowerLevelsEventContent, this code panics.
206         var withDefaults powerLevelsWithDefaults
207         src := reflect.ValueOf(roomLevels).Elem()
208         dst := reflect.ValueOf(&withDefaults).Elem()
209         for i := 0; i < src.Type().NumField(); i++ {
210                 srcField := src.Type().Field(i)
211                 if !srcField.IsExported() {
212                         continue
213                 }
214                 dst.FieldByName(srcField.Name).Set(src.Field(i))
215         }
216         return &withDefaults
217 }