From 6008464684c93f3bf0ccd1e68b8d1f9717211efb Mon Sep 17 00:00:00 2001 From: Steve Date: Sun, 5 Apr 2020 18:50:32 -0400 Subject: [PATCH] initial echo bot powers --- botlib/custom_syncer.go | 173 ++++++++++++++++++++++++++++++++++++++++ botlib/main.go | 129 ++++++++++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 botlib/custom_syncer.go create mode 100644 botlib/main.go diff --git a/botlib/custom_syncer.go b/botlib/custom_syncer.go new file mode 100644 index 0000000..9081aa0 --- /dev/null +++ b/botlib/custom_syncer.go @@ -0,0 +1,173 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "runtime/debug" + "time" + + "maunium.net/go/mautrix" + . "maunium.net/go/mautrix" +) + +// CustomSyncer is the default syncing implementation. You can either write your own syncer, or selectively +// replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer +// pattern to notify callers about incoming events. See CustomSyncer.OnEventType for more information. +type CustomSyncer struct { + UserID string + Store Storer + listeners map[EventType][]OnEventListener // event type to listeners array +} + +// OnEventListener can be used with CustomSyncer.OnEventType to be informed of incoming events. +type OnEventListener func(*Event) + +// NewCustomSyncer returns an instantiated CustomSyncer +func NewCustomSyncer(userID string, store Storer) *CustomSyncer { + return &CustomSyncer{ + UserID: userID, + Store: store, + listeners: make(map[EventType][]mautrix.OnEventListener), + } +} + +func parseEvent(roomID string, data json.RawMessage) *Event { + event := &Event{} + err := json.Unmarshal(data, event) + if err != nil { + // TODO add separate handler for these + _, _ = fmt.Fprintf(os.Stderr, "Failed to unmarshal event: %v\n%s\n", err, string(data)) + return nil + } + if roomID != "" && event.RoomID != roomID { + event.RoomID = roomID + } + return event +} + +// ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of +// unrepeating events. Returns a fatal error if a listener panics. +func (s *CustomSyncer) ProcessResponse(res *RespSync, since string) (err error) { + if !s.shouldProcessResponse(res, since) { + return + } + + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.UserID, since, r, debug.Stack()) + } + }() + + for roomID, roomData := range res.Rooms.Join { + room := s.getOrCreateRoom(roomID) + for _, data := range roomData.State.Events { + event := parseEvent(roomID, data) + if event != nil { + room.UpdateState(event) + s.notifyListeners(event) + } + } + for _, data := range roomData.Timeline.Events { + event := parseEvent(roomID, data) + if event != nil { + s.notifyListeners(event) + } + } + } + for roomID, roomData := range res.Rooms.Invite { + room := s.getOrCreateRoom(roomID) + for _, data := range roomData.State.Events { + event := parseEvent(roomID, data) + if event != nil { + room.UpdateState(event) + s.notifyListeners(event) + } + } + } + for roomID, roomData := range res.Rooms.Leave { + room := s.getOrCreateRoom(roomID) + for _, data := range roomData.Timeline.Events { + event := parseEvent(roomID, data) + if event.StateKey != nil { + room.UpdateState(event) + s.notifyListeners(event) + } + } + } + return +} + +// OnEventType allows callers to be notified when there are new events for the given event type. +// There are no duplicate checks. +func (s *CustomSyncer) OnEventType(eventType EventType, callback mautrix.OnEventListener) { + _, exists := s.listeners[eventType] + if !exists { + s.listeners[eventType] = []mautrix.OnEventListener{} + } + s.listeners[eventType] = append(s.listeners[eventType], callback) +} + +// shouldProcessResponse returns true if the response should be processed. May modify the response to remove +// stuff that shouldn't be processed. +func (s *CustomSyncer) shouldProcessResponse(resp *RespSync, since string) bool { + if since == "" { + return false + } + // This is a horrible hack because /sync will return the most recent messages for a room + // as soon as you /join it. We do NOT want to process those events in that particular room + // because they may have already been processed (if you toggle the bot in/out of the room). + // + // Work around this by inspecting each room's timeline and seeing if an m.room.member event for us + // exists and is "join" and then discard processing that room entirely if so. + // TODO: We probably want to process messages from after the last join event in the timeline. + for roomID, roomData := range resp.Rooms.Join { + for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- { + evtData := roomData.Timeline.Events[i] + // TODO this is horribly inefficient since it's also parsed in ProcessResponse + e := parseEvent(roomID, evtData) + if e != nil && e.Type == StateMember && e.GetStateKey() == s.UserID { + if e.Content.Membership == "join" { + _, ok := resp.Rooms.Join[roomID] + if !ok { + continue + } + delete(resp.Rooms.Join, roomID) // don't re-process messages + delete(resp.Rooms.Invite, roomID) // don't re-process invites + break + } + } + } + } + return true +} + +// getOrCreateRoom must only be called by the Sync() goroutine which calls ProcessResponse() +func (s *CustomSyncer) getOrCreateRoom(roomID string) *Room { + room := s.Store.LoadRoom(roomID) + if room == nil { // create a new Room + room = NewRoom(roomID) + s.Store.SaveRoom(room) + } + return room +} + +func (s *CustomSyncer) notifyListeners(event *Event) { + listeners, exists := s.listeners[event.Type] + if !exists { + return + } + for _, fn := range listeners { + fn(event) + } +} + +// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error. +func (s *CustomSyncer) OnFailedSync(res *RespSync, err error) (time.Duration, error) { + return 10 * time.Second, nil +} + +// GetFilterJSON returns a filter with a timeline limit of 50. +func (s *CustomSyncer) GetFilterJSON(userID string) json.RawMessage { + return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`) +} diff --git a/botlib/main.go b/botlib/main.go new file mode 100644 index 0000000..c5b5670 --- /dev/null +++ b/botlib/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "log" + "time" + + "maunium.net/go/mautrix" +) + +type Config struct { + Userid string + Server string + Token string + Owner string +} + +type Bot struct { + Client *mautrix.Client + Rooms map[string]int + Conf *Config +} + +func main() { + c := &Config{ + Userid: "@testbot:saintnet.tech", + Server: "matrix.saintnet.tech", + Token: "MDAxYmxvY2F0aW9uIHNhaW50bmV0LnRlY2gKMDAxM2lkZW50aWZpZXIga2V5CjAwMTBjaWQgZ2VuID0gMQowMDI5Y2lkIHVzZXJfaWQgPSBAdGVzdGJvdDpzYWludG5ldC50ZWNoCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gemlKZGs9bmJhcjp1eThIXgowMDJmc2lnbmF0dXJlIE4x0sbUS9lN-fi0KjJmEmpx6_wpYzgvk4k2Eugtkva7Cg", + Owner: "@stryan:saintnet.tech", + } + b := NewBot(c) + syncer := NewCustomSyncer("@testbot:saintnet.tech", b.Client.Store) + syncer.OnEventType(mautrix.EventMessage, b.handleMessage) + syncer.OnEventType(mautrix.StateMember, b.handleMember) + + b.Client.Syncer = syncer + go func() { + err := b.Client.Sync() + if err != nil { + log.Fatal(err) + } + }() + fmt.Println("Syncing enabled") + resp, err := b.Client.JoinedRooms() + if err != nil { + log.Fatal(err) + } + for _, v := range resp.JoinedRooms { + //fmt.Printf("Bot is in Room %v\n", v) + + mem, err := b.Client.JoinedMembers(v) + if err != nil { + log.Fatal(err) + } + if len(mem.Joined) > 1 { + b.Rooms[v] = len(mem.Joined) + fmt.Printf("Bot is in Room %v\n", v) + } else { + _, err := b.Client.LeaveRoom(v) + if err != nil { + log.Fatal(err) + } + _, err = b.Client.ForgetRoom(v) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Bot was in Room %v, left due to being last one there\n", v) + } + + } + fmt.Println("Begining main loop") + for { + time.Sleep(2 * time.Second) + fmt.Println("Checking") + } + +} + +func NewBot(c *Config) *Bot { + client, err := mautrix.NewClient(c.Server, c.Userid, c.Token) + if err != nil { + log.Fatal(err) + } + b := &Bot{Client: client, Rooms: make(map[string]int), Conf: c} + return b +} + +func (b *Bot) handleMessage(e *mautrix.Event) { + if e.Sender == b.Conf.Userid { + return //we don't care about our own messages + } + body := e.Content.Body + _, err := b.Client.SendText(e.RoomID, body) + if err != nil { + log.Fatal(err) + } +} + +func (b *Bot) handleMember(e *mautrix.Event) { + mbr := e.Content.Membership + if mbr == mautrix.MembershipInvite { + if e.Sender == b.Conf.Owner && e.GetStateKey() == b.Conf.Userid { + fmt.Println("Joining room:", e.RoomID) + _, err := b.Client.JoinRoom(e.RoomID, "", nil) + if err != nil { + log.Fatal(err) + } + + } + } + if mbr == mautrix.MembershipJoin { + log.Println("A user joined a room we care about") + b.Rooms[e.RoomID] = b.Rooms[e.RoomID] + 1 + } + if mbr == mautrix.MembershipLeave && e.Sender != b.Conf.Userid { + b.Rooms[e.RoomID] = b.Rooms[e.RoomID] - 1 + if b.Rooms[e.RoomID] < 2 { + log.Println("Last in room, leaving") + _, err := b.Client.LeaveRoom(e.RoomID) + if err != nil { + log.Fatal(err) + } + _, err = b.Client.ForgetRoom(e.RoomID) + if err != nil { + log.Fatal(err) + } + } + } +}