]> de.git.xonotic.org Git - xonotic/xonotic.git/blob - misc/infrastructure/powerbot/bot.go
demotc-race-record-extractor.sh: remove useless rc case (gametype name is always...
[xonotic/xonotic.git] / misc / infrastructure / powerbot / bot.go
1 package main
2
3 import (
4         "encoding/json"
5         "fmt"
6         "io/ioutil"
7         "log"
8         "maunium.net/go/mautrix"
9         "maunium.net/go/mautrix/event"
10         "maunium.net/go/mautrix/id"
11         "strings"
12         "sync"
13         "time"
14 )
15
16 const (
17         syncInterval       = time.Minute
18         syncForceFrequency = int(7 * 24 * time.Hour / syncInterval)
19 )
20
21 type Room struct {
22         ID   id.RoomID `json:"id"`
23         Name string    `json:"name",omitempty`
24 }
25
26 type Config struct {
27         Homeserver  string      `json:"homeserver"`
28         UserID      id.UserID   `json:"user_id"`
29         Password    string      `json:"password,omitempty"`
30         DeviceID    id.DeviceID `json:"device_id,omitempty"`
31         AccessToken string      `json:"access_token,omitempty"`
32         Rooms       [][]Room    `json:"rooms"`
33 }
34
35 func (c *Config) Load() error {
36         log.Printf("Loading config.")
37         data, err := ioutil.ReadFile("config.json")
38         if err != nil {
39                 return err
40         }
41         return json.Unmarshal(data, c)
42 }
43
44 func (c *Config) Save() error {
45         log.Printf("Saving config.")
46         data, err := json.MarshalIndent(c, "", "\t")
47         if err != nil {
48                 return err
49         }
50         return ioutil.WriteFile("config.json", data, 0700)
51 }
52
53 func Login(config *Config) (*mautrix.Client, error) {
54         configMu.Lock()
55         defer configMu.Unlock()
56
57         // Note: we have to lower case the user ID for Matrix protocol communication.
58         uid := id.UserID(strings.ToLower(string(config.UserID)))
59         client, err := mautrix.NewClient(config.Homeserver, uid, config.AccessToken)
60         if err != nil {
61                 return nil, fmt.Errorf("failed to create client: %v", err)
62         }
63         if config.AccessToken == "" {
64                 resp, err := client.Login(&mautrix.ReqLogin{
65                         Type: mautrix.AuthTypePassword,
66                         Identifier: mautrix.UserIdentifier{
67                                 Type: mautrix.IdentifierTypeUser,
68                                 User: string(client.UserID),
69                         },
70                         Password:                 config.Password,
71                         InitialDeviceDisplayName: "matrixbot",
72                         StoreCredentials:         true,
73                 })
74                 if err != nil {
75                         return nil, fmt.Errorf("failed to authenticate: %v", err)
76                 }
77                 config.Password = ""
78                 config.DeviceID = resp.DeviceID
79                 config.AccessToken = resp.AccessToken
80                 err = config.Save()
81                 if err != nil {
82                         return nil, fmt.Errorf("failed to save config: %v", err)
83                 }
84         } else {
85                 client.DeviceID = config.DeviceID
86         }
87         return client, nil
88 }
89
90 var (
91         configMu sync.Mutex
92
93         roomUsersMu sync.RWMutex
94         roomUsers   = map[id.RoomID]map[id.UserID]struct{}{}
95
96         fullySynced     bool
97         roomPowerLevels = map[id.RoomID]*event.PowerLevelsEventContent{}
98 )
99
100 func setUserStateAt(room id.RoomID, user id.UserID, now time.Time, maxPrevState, state State) {
101         err := writeUserStateAt(room, user, now, maxPrevState, state)
102         if err != nil {
103                 log.Fatalf("failed to write user state: %v", err)
104         }
105 }
106
107 func handleMessage(now time.Time, room id.RoomID, sender id.UserID, raw *event.Event) {
108         // log.Printf("[%v] Message from %v to %v", now, sender, room)
109         roomUsersMu.Lock()
110         roomUsers[room][sender] = struct{}{}
111         roomUsersMu.Unlock()
112         setUserStateAt(room, sender, now.Add(-activeTime), Active, Active)
113         setUserStateAt(room, sender, now, Active, Idle)
114 }
115
116 func handleJoin(now time.Time, room id.RoomID, member id.UserID, raw *event.Event) {
117         log.Printf("[%v] Join from %v to %v", now, member, room)
118         roomUsersMu.Lock()
119         roomUsers[room][member] = struct{}{}
120         roomUsersMu.Unlock()
121         setUserStateAt(room, member, now, NotActive, Idle)
122 }
123
124 func handleLeave(now time.Time, room id.RoomID, member id.UserID, raw *event.Event) {
125         log.Printf("[%v] Leave from %v to %v", now, member, room)
126         roomUsersMu.Lock()
127         delete(roomUsers[room], member)
128         roomUsersMu.Unlock()
129         setUserStateAt(room, member, now, Active, NotActive)
130 }
131
132 func handlePowerLevels(now time.Time, room id.RoomID, levels *event.PowerLevelsEventContent, raw *event.Event) {
133         // log.Printf("[%v] Power levels for %v are %v", now, room, levels)
134         levelsCopy := *levels // Looks like mautrix always passes the same pointer here.
135         roomUsersMu.Lock()
136         roomPowerLevels[room] = &levelsCopy
137         roomUsersMu.Unlock()
138 }
139
140 func eventTime(evt *event.Event) time.Time {
141         return time.Unix(0, evt.Timestamp*1000000)
142 }
143
144 type MoreMessagesSyncer struct {
145         *mautrix.DefaultSyncer
146 }
147
148 func newSyncer() *MoreMessagesSyncer {
149         return &MoreMessagesSyncer{
150                 DefaultSyncer: mautrix.NewDefaultSyncer(),
151         }
152 }
153
154 func (s *MoreMessagesSyncer) GetFilterJSON(userID id.UserID) *mautrix.Filter {
155         f := s.DefaultSyncer.GetFilterJSON(userID)
156         // Same filters as Element.
157         f.Room.Timeline.Limit = 20
158         // Only include our rooms.
159         f.Room.Rooms = make([]id.RoomID, 0, len(roomUsers))
160         for room := range roomUsers {
161                 f.Room.Rooms = append(f.Room.Rooms, room)
162         }
163         return f
164 }
165
166 func isRoom(room id.RoomID) bool {
167         roomUsersMu.RLock()
168         defer roomUsersMu.RUnlock()
169         _, found := roomUsers[room]
170         return found
171 }
172
173 func Run() (err error) {
174         err = InitDatabase()
175         if err != nil {
176                 return fmt.Errorf("failed to init database: %v", err)
177         }
178         defer func() {
179                 err2 := CloseDatabase()
180                 if err2 != nil && err == nil {
181                         err = fmt.Errorf("failed to close database: %v", err)
182                 }
183         }()
184         logPowerLevelBounds()
185         config := &Config{}
186         err = config.Load()
187         if err != nil {
188                 return fmt.Errorf("failed to load config: %v", err)
189         }
190         for _, group := range config.Rooms {
191                 for _, room := range group {
192                         roomUsers[room.ID] = map[id.UserID]struct{}{}
193                 }
194         }
195         client, err := Login(config)
196         if err != nil {
197                 return fmt.Errorf("failed to login: %v", err)
198         }
199         syncer := newSyncer()
200         syncer.OnEventType(event.StateTombstone, func(source mautrix.EventSource, evt *event.Event) {
201                 if !isRoom(evt.RoomID) {
202                         return
203                 }
204                 tomb := evt.Content.AsTombstone()
205                 if tomb.ReplacementRoom == "" {
206                         log.Printf("Replacement room in tombstone event is not set - not handling: %v", evt)
207                         return
208                 }
209                 for _, group := range config.Rooms {
210                         for i := range group {
211                                 room := &group[i]
212                                 if room.ID == evt.RoomID {
213                                         configMu.Lock()
214                                         defer configMu.Unlock()
215                                         room.ID = tomb.ReplacementRoom
216                                         err := config.Save()
217                                         if err != nil {
218                                                 log.Printf("failed to save config: %v", err)
219                                         }
220                                         log.Fatalf("room upgrade for %v handled from %v to %v - need restart", room.Name, evt.RoomID, tomb.ReplacementRoom)
221                                 }
222                         }
223                 }
224                 log.Printf("Room not found in config, so not doing room upgrade: %v", evt)
225         })
226         syncer.OnEventType(event.EventMessage, func(source mautrix.EventSource, evt *event.Event) {
227                 if !isRoom(evt.RoomID) {
228                         return
229                 }
230                 handleMessage(eventTime(evt), evt.RoomID, evt.Sender, evt)
231         })
232         syncer.OnEventType(event.StateMember, func(source mautrix.EventSource, evt *event.Event) {
233                 if !isRoom(evt.RoomID) {
234                         return
235                 }
236                 mem := evt.Content.AsMember()
237                 key := evt.StateKey
238                 if key == nil {
239                         return
240                 }
241                 member := id.UserID(*key)
242                 switch mem.Membership {
243                 case event.MembershipJoin:
244                         handleJoin(eventTime(evt), evt.RoomID, member, evt)
245                 case event.MembershipLeave, event.MembershipBan:
246                         handleLeave(eventTime(evt), evt.RoomID, member, evt)
247                 default: // Ignore.
248                 }
249         })
250         syncer.OnEventType(event.StatePowerLevels, func(source mautrix.EventSource, evt *event.Event) {
251                 if !isRoom(evt.RoomID) {
252                         return
253                 }
254                 handlePowerLevels(eventTime(evt), evt.RoomID, evt.Content.AsPowerLevels(), evt)
255         })
256         syncer.OnSync(func(resp *mautrix.RespSync, since string) bool {
257                 // j, _ := json.MarshalIndent(resp, "", "  ")
258                 // log.Print(string(j))
259                 roomUsersMu.Lock()
260                 if since != "" && !fullySynced {
261                         log.Print("Fully synced.")
262                         for room, users := range roomUsers {
263                                 if _, found := users[config.UserID]; !found {
264                                         log.Printf("Not actually joined %v yet...", room)
265                                         _, err := client.JoinRoom(string(room), "", nil)
266                                         if err != nil {
267                                                 log.Printf("Failed to join %v: %v", room, err)
268                                         }
269                                 }
270                         }
271                         fullySynced = true
272                 }
273                 roomUsersMu.Unlock()
274                 return true
275         })
276         client.Syncer = syncer
277         ticker := time.NewTicker(syncInterval)
278         defer ticker.Stop()
279         go func() {
280                 counter := 0
281                 for range ticker.C {
282                         roomUsersMu.RLock()
283                         scoreData := map[id.RoomID]map[id.UserID]*Score{}
284                         now := time.Now()
285                         for room := range roomUsers {
286                                 scores, err := queryUserScores(room, now)
287                                 if err != nil {
288                                         log.Fatalf("failed to query user scores: %v", err)
289                                 }
290                                 scoreData[room] = scores
291                         }
292                         for _, group := range config.Rooms {
293                                 for _, room := range group {
294                                         syncPowerLevels(client, room.ID, group, scoreData, counter%syncForceFrequency == 0)
295                                 }
296                         }
297                         roomUsersMu.RUnlock()
298                         counter++
299                 }
300         }()
301         return client.Sync()
302 }
303
304 func main() {
305         err := Run()
306         if err != nil {
307                 log.Fatalf("Program failed: %v", err)
308         }
309 }