initial echo bot powers
This commit is contained in:
commit
6008464684
173
botlib/custom_syncer.go
Normal file
173
botlib/custom_syncer.go
Normal file
@ -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}}}`)
|
||||
}
|
129
botlib/main.go
Normal file
129
botlib/main.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user