5 "github.com/google/go-cmp/cmp"
8 "maunium.net/go/mautrix"
9 "maunium.net/go/mautrix/event"
10 "maunium.net/go/mautrix/id"
18 activeTime = 5 * time.Minute
19 // 15 minutes idling = PL 1.
20 minPowerScore = 15 * 60 * idleScore
22 // 1 year fulltime active dev = PL 10.
23 maxPowerScore = 3600 * (365*24*idleScore + 8*261*(activeScore-idleScore))
25 // Do not touch users outside this range.
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
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).",
39 time.Duration(float64(time.Second)*score/idleScore),
40 time.Duration(float64(time.Second)*score/activeScore),
45 func computePowerLevel(def int, score Score) (int, float64) {
46 points := score.Idle.Seconds()*idleScore + score.Active.Seconds()*activeScore
48 return def, math.Inf(-1)
50 raw := minPowerLevel + (maxPowerLevel-minPowerLevel)*math.Log(points/minPowerScore)/math.Log(maxPowerScore/minPowerScore)
51 if raw < minPowerLevel {
54 if points > maxPowerScore {
55 return maxPowerLevel, raw
57 return int(math.Floor(raw)), raw
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)
65 ret = append(ret, roomLevels.EventsDefault)
66 if roomLevels.InvitePtr != nil {
67 ret = append(ret, *roomLevels.InvitePtr)
69 if roomLevels.KickPtr != nil {
70 ret = append(ret, *roomLevels.KickPtr)
72 if roomLevels.BanPtr != nil {
73 ret = append(ret, *roomLevels.BanPtr)
75 if roomLevels.RedactPtr != nil {
76 ret = append(ret, *roomLevels.RedactPtr)
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)
88 for _, level := range allPowerLevels(roomLevels) {
89 if minPowerLevel <= level && level <= maxPowerLevel {
94 log.Printf("room %v skipping because PLs currently do not matter", room)
97 log.Printf("room %v considering to update PLs", room)
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
109 newRoomLevels := makeDefaultsExplicit(roomLevels)
110 newRoomLevels.Users = make(map[id.UserID]int)
111 for user, level := range roomLevels.Users {
112 if level == roomLevels.UsersDefault {
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)
123 newRoomLevels.Users[user] = level
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.
132 prevLevel := roomLevels.Users[user]
133 level, raw := computePowerLevel(roomLevels.UsersDefault, *score)
134 for _, otherRoom := range roomGroup {
135 if otherRoom.ID == room {
138 otherScore := scores[otherRoom.ID][user]
139 if otherScore == nil {
142 otherLevel, otherRaw := computePowerLevel(roomLevels.UsersDefault, *otherScore)
143 if otherLevel > level {
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
161 log.Printf("room %v user %v power level: KEEP %v -> %v (%v, %v)", room, user, prevLevel, level, raw, score)
164 clearPowerLevel := minPowerLevel
165 for clearPowerLevel <= maxPowerLevel {
166 j, err := json.Marshal(newRoomLevels)
168 log.Printf("could not marshal newRoomLevels: %v", err)
171 if len(j) <= maxPowerLevelBytes {
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)
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)
188 log.Printf("Failed to update power levels: %v", err)
191 log.Printf("room %v nothing to update", room)
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"`
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() {
223 dst.FieldByName(srcField.Name).Set(src.Field(i))