8 "maunium.net/go/mautrix"
9 "maunium.net/go/mautrix/event"
10 "maunium.net/go/mautrix/id"
17 syncInterval = time.Minute
18 syncForceFrequency = int(7 * 24 * time.Hour / syncInterval)
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"`
35 func (c *Config) Load() error {
36 log.Printf("Loading config.")
37 data, err := ioutil.ReadFile("config.json")
41 return json.Unmarshal(data, c)
44 func (c *Config) Save() error {
45 log.Printf("Saving config.")
46 data, err := json.MarshalIndent(c, "", "\t")
50 return ioutil.WriteFile("config.json", data, 0700)
53 func Login(config *Config) (*mautrix.Client, error) {
55 defer configMu.Unlock()
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)
61 return nil, fmt.Errorf("failed to create client: %v", err)
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),
70 Password: config.Password,
71 InitialDeviceDisplayName: "matrixbot",
72 StoreCredentials: true,
75 return nil, fmt.Errorf("failed to authenticate: %v", err)
78 config.DeviceID = resp.DeviceID
79 config.AccessToken = resp.AccessToken
82 return nil, fmt.Errorf("failed to save config: %v", err)
85 client.DeviceID = config.DeviceID
93 roomUsersMu sync.RWMutex
94 roomUsers = map[id.RoomID]map[id.UserID]struct{}{}
97 roomPowerLevels = map[id.RoomID]*event.PowerLevelsEventContent{}
100 func setUserStateAt(room id.RoomID, user id.UserID, now time.Time, maxPrevState, state State) {
101 err := writeUserStateAt(room, user, now, maxPrevState, state)
103 log.Fatalf("failed to write user state: %v", err)
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)
110 roomUsers[room][sender] = struct{}{}
112 setUserStateAt(room, sender, now.Add(-activeTime), Active, Active)
113 setUserStateAt(room, sender, now, Active, Idle)
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)
119 roomUsers[room][member] = struct{}{}
121 setUserStateAt(room, member, now, NotActive, Idle)
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)
127 delete(roomUsers[room], member)
129 setUserStateAt(room, member, now, Active, NotActive)
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.
136 roomPowerLevels[room] = &levelsCopy
140 func eventTime(evt *event.Event) time.Time {
141 return time.Unix(0, evt.Timestamp*1000000)
144 type MoreMessagesSyncer struct {
145 *mautrix.DefaultSyncer
148 func newSyncer() *MoreMessagesSyncer {
149 return &MoreMessagesSyncer{
150 DefaultSyncer: mautrix.NewDefaultSyncer(),
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)
166 func isRoom(room id.RoomID) bool {
168 defer roomUsersMu.RUnlock()
169 _, found := roomUsers[room]
173 func Run() (err error) {
176 return fmt.Errorf("failed to init database: %v", err)
179 err2 := CloseDatabase()
180 if err2 != nil && err == nil {
181 err = fmt.Errorf("failed to close database: %v", err)
184 logPowerLevelBounds()
188 return fmt.Errorf("failed to load config: %v", err)
190 for _, group := range config.Rooms {
191 for _, room := range group {
192 roomUsers[room.ID] = map[id.UserID]struct{}{}
195 client, err := Login(config)
197 return fmt.Errorf("failed to login: %v", err)
199 syncer := newSyncer()
200 syncer.OnEventType(event.StateTombstone, func(source mautrix.EventSource, evt *event.Event) {
201 if !isRoom(evt.RoomID) {
204 tomb := evt.Content.AsTombstone()
205 if tomb.ReplacementRoom == "" {
206 log.Printf("Replacement room in tombstone event is not set - not handling: %v", evt)
209 for _, group := range config.Rooms {
210 for _, room := range group {
211 if room.ID == evt.RoomID {
213 defer configMu.Unlock()
214 room.ID = tomb.ReplacementRoom
216 log.Fatalf("room upgrade handled from %v to %v - need restart", evt.RoomID, tomb.ReplacementRoom)
221 syncer.OnEventType(event.EventMessage, func(source mautrix.EventSource, evt *event.Event) {
222 if !isRoom(evt.RoomID) {
225 handleMessage(eventTime(evt), evt.RoomID, evt.Sender, evt)
227 syncer.OnEventType(event.StateMember, func(source mautrix.EventSource, evt *event.Event) {
228 if !isRoom(evt.RoomID) {
231 mem := evt.Content.AsMember()
236 member := id.UserID(*key)
237 switch mem.Membership {
238 case event.MembershipJoin:
239 handleJoin(eventTime(evt), evt.RoomID, member, evt)
240 case event.MembershipLeave, event.MembershipBan:
241 handleLeave(eventTime(evt), evt.RoomID, member, evt)
245 syncer.OnEventType(event.StatePowerLevels, func(source mautrix.EventSource, evt *event.Event) {
246 if !isRoom(evt.RoomID) {
249 handlePowerLevels(eventTime(evt), evt.RoomID, evt.Content.AsPowerLevels(), evt)
251 syncer.OnSync(func(resp *mautrix.RespSync, since string) bool {
252 // j, _ := json.MarshalIndent(resp, "", " ")
253 // log.Print(string(j))
255 if since != "" && !fullySynced {
256 log.Print("Fully synced.")
257 for room, users := range roomUsers {
258 if _, found := users[config.UserID]; !found {
259 log.Printf("Not actually joined %v yet...", room)
260 _, err := client.JoinRoom(string(room), "", nil)
262 log.Printf("Failed to join %v: %v", room, err)
271 client.Syncer = syncer
272 ticker := time.NewTicker(syncInterval)
278 scoreData := map[id.RoomID]map[id.UserID]*Score{}
280 for room := range roomUsers {
281 scores, err := queryUserScores(room, now)
283 log.Fatalf("failed to query user scores: %v", err)
285 scoreData[room] = scores
287 for _, group := range config.Rooms {
288 for _, room := range group {
289 syncPowerLevels(client, room.ID, group, scoreData, counter%syncForceFrequency == 0)
292 roomUsersMu.RUnlock()
302 log.Fatalf("Program failed: %v", err)