8 "maunium.net/go/mautrix"
9 "maunium.net/go/mautrix/event"
10 "maunium.net/go/mautrix/id"
17 syncInterval = time.Minute
18 syncForceFrequency = 7 * 24 * time.Hour / syncInterval
22 Homeserver string `json:"homeserver"`
23 UserID id.UserID `json:"user_id"`
24 Password string `json:"password,omitempty"`
25 DeviceID id.DeviceID `json:"device_id,omitempty"`
26 AccessToken string `json:"access_token,omitempty"`
27 Rooms [][]id.RoomID `json:"rooms"`
30 func (c *Config) Load() error {
31 log.Printf("Loading config.")
32 data, err := ioutil.ReadFile("config.json")
36 return json.Unmarshal(data, c)
39 func (c *Config) Save() error {
40 log.Printf("Saving config.")
41 data, err := json.MarshalIndent(c, "", "\t")
45 return ioutil.WriteFile("config.json", data, 0700)
48 func Login(config *Config) (*mautrix.Client, error) {
49 // Note: we have to lower case the user ID for Matrix protocol communication.
50 uid := id.UserID(strings.ToLower(string(config.UserID)))
51 client, err := mautrix.NewClient(config.Homeserver, uid, config.AccessToken)
53 return nil, fmt.Errorf("failed to create client: %v", err)
55 if config.AccessToken == "" {
56 resp, err := client.Login(&mautrix.ReqLogin{
57 Type: mautrix.AuthTypePassword,
58 Identifier: mautrix.UserIdentifier{
59 Type: mautrix.IdentifierTypeUser,
60 User: string(client.UserID),
62 Password: config.Password,
63 InitialDeviceDisplayName: "matrixbot",
64 StoreCredentials: true,
67 return nil, fmt.Errorf("failed to authenticate: %v", err)
70 config.DeviceID = resp.DeviceID
71 config.AccessToken = resp.AccessToken
74 return nil, fmt.Errorf("failed to save config: %v", err)
77 client.DeviceID = config.DeviceID
83 roomUsers = map[id.RoomID]map[id.UserID]struct{}{}
84 roomUsersMu sync.RWMutex
86 roomPowerLevels = map[id.RoomID]*event.PowerLevelsEventContent{}
89 func setUserStateAt(room id.RoomID, user id.UserID, now time.Time, maxPrevState, state State) {
90 err := writeUserStateAt(room, user, now, maxPrevState, state)
92 log.Fatalf("failed to write user state: %v", err)
96 func handleMessage(now time.Time, room id.RoomID, sender id.UserID, raw *event.Event) {
97 // log.Printf("[%v] Message from %v to %v", now, sender, room)
99 roomUsers[room][sender] = struct{}{}
101 setUserStateAt(room, sender, now.Add(-activeTime), Active, Active)
102 setUserStateAt(room, sender, now, Active, Idle)
105 func handleJoin(now time.Time, room id.RoomID, member id.UserID, raw *event.Event) {
106 log.Printf("[%v] Join from %v to %v", now, member, room)
108 roomUsers[room][member] = struct{}{}
110 setUserStateAt(room, member, now, NotActive, Idle)
113 func handleLeave(now time.Time, room id.RoomID, member id.UserID, raw *event.Event) {
114 log.Printf("[%v] Leave from %v to %v", now, member, room)
116 delete(roomUsers[room], member)
118 setUserStateAt(room, member, now, Active, NotActive)
121 func handlePowerLevels(now time.Time, room id.RoomID, levels *event.PowerLevelsEventContent, raw *event.Event) {
122 // log.Printf("[%v] Power levels for %v are %v", now, room, levels)
123 levelsCopy := *levels // Looks like mautrix always passes the same pointer here.
125 roomPowerLevels[room] = &levelsCopy
129 func eventTime(evt *event.Event) time.Time {
130 return time.Unix(0, evt.Timestamp*1000000)
133 type MoreMessagesSyncer struct {
134 *mautrix.DefaultSyncer
137 func newSyncer() *MoreMessagesSyncer {
138 return &MoreMessagesSyncer{
139 DefaultSyncer: mautrix.NewDefaultSyncer(),
143 func (s *MoreMessagesSyncer) GetFilterJSON(userID id.UserID) *mautrix.Filter {
144 f := s.DefaultSyncer.GetFilterJSON(userID)
145 // Same filters as Element.
146 f.Room.Timeline.Limit = 20
147 // Only include our rooms.
148 f.Room.Rooms = make([]id.RoomID, 0, len(roomUsers))
149 for room := range roomUsers {
150 f.Room.Rooms = append(f.Room.Rooms, room)
155 func isRoom(room id.RoomID) bool {
157 defer roomUsersMu.RUnlock()
158 _, found := roomUsers[room]
162 func Run() (err error) {
165 return fmt.Errorf("failed to init database: %v", err)
168 err2 := CloseDatabase()
169 if err2 != nil && err == nil {
170 err = fmt.Errorf("failed to close database: %v", err)
173 logPowerLevelBounds()
177 return fmt.Errorf("failed to load config: %v", err)
179 for _, group := range config.Rooms {
180 for _, room := range group {
181 roomUsers[room] = map[id.UserID]struct{}{}
184 client, err := Login(config)
186 return fmt.Errorf("failed to login: %v", err)
188 syncer := newSyncer()
189 syncer.OnEventType(event.EventMessage, func(source mautrix.EventSource, evt *event.Event) {
190 if !isRoom(evt.RoomID) {
193 handleMessage(eventTime(evt), evt.RoomID, evt.Sender, evt)
195 syncer.OnEventType(event.StateMember, func(source mautrix.EventSource, evt *event.Event) {
196 if !isRoom(evt.RoomID) {
199 mem := evt.Content.AsMember()
204 member := id.UserID(*key)
205 switch mem.Membership {
206 case event.MembershipJoin:
207 handleJoin(eventTime(evt), evt.RoomID, member, evt)
208 case event.MembershipLeave, event.MembershipBan:
209 handleLeave(eventTime(evt), evt.RoomID, member, evt)
213 syncer.OnEventType(event.StatePowerLevels, func(source mautrix.EventSource, evt *event.Event) {
214 if !isRoom(evt.RoomID) {
217 handlePowerLevels(eventTime(evt), evt.RoomID, evt.Content.AsPowerLevels(), evt)
219 syncer.OnSync(func(resp *mautrix.RespSync, since string) bool {
220 // j, _ := json.MarshalIndent(resp, "", " ")
221 // log.Print(string(j))
223 if since != "" && !fullySynced {
224 log.Print("Fully synced.")
225 for room, users := range roomUsers {
226 if _, found := users[config.UserID]; !found {
227 log.Printf("Not actually joined %v yet...", room)
228 _, err := client.JoinRoom(string(room), "", nil)
230 log.Printf("Failed to join %v: %v", room, err)
239 client.Syncer = syncer
240 ticker := time.NewTicker(syncInterval)
246 scoreData := map[id.RoomID]map[id.UserID]*Score{}
248 for room := range roomUsers {
249 scores, err := queryUserScores(room, now)
251 log.Fatalf("failed to query user scores: %v", err)
253 scoreData[room] = scores
255 for _, group := range config.Rooms {
256 for _, room := range group {
257 syncPowerLevels(client, room, group, scoreData, counter%syncForceFrequency == 0)
260 roomUsersMu.RUnlock()
270 log.Fatalf("Program failed: %v", err)