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