diff --git a/bridge/matrix.go b/bridge/matrix.go index 9e305d0..7e4686a 100644 --- a/bridge/matrix.go +++ b/bridge/matrix.go @@ -199,11 +199,11 @@ func (mh *matrixHandler) handleMembership(evt *event.Event) { return } + puppet := mh.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) + // Load or create a new portal. portal := mh.bridge.GetPortalByMXID(evt.RoomID) if portal == nil { - puppet := mh.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) - if content.Membership == event.MembershipInvite && puppet != nil { mh.handlePuppetInvite(evt, user, puppet) } @@ -213,7 +213,20 @@ func (mh *matrixHandler) handleMembership(evt *event.Event) { isSelf := id.UserID(evt.GetStateKey()) == evt.Sender - if content.Membership == event.MembershipInvite && !isSelf { - portal.HandleMatrixInvite(user, evt) + if content.Membership == event.MembershipLeave { + if evt.Unsigned.PrevContent != nil { + _ = evt.Unsigned.PrevContent.ParseRaw(evt.Type) + prevContent, ok := evt.Unsigned.PrevContent.Parsed.(*event.MemberEventContent) + if ok && prevContent.Membership != "join" { + return + } + } + if isSelf { + portal.handleMatrixLeave(user) + } else if puppet != nil { + portal.handleMatrixKick(user, puppet) + } + } else if content.Membership == event.MembershipInvite && !isSelf { + portal.handleMatrixInvite(user, evt) } } diff --git a/bridge/portal.go b/bridge/portal.go index 9f7bd88..620fc8e 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -136,7 +136,7 @@ func (b *Bridge) NewPortal(dbPortal *database.Portal) *Portal { return portal } -func (p *Portal) HandleMatrixInvite(sender *User, evt *event.Event) { +func (p *Portal) handleMatrixInvite(sender *User, evt *event.Event) { // Look up an existing puppet or create a new one. puppet := p.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) if puppet != nil { @@ -354,3 +354,113 @@ func (p *Portal) handleMatrixMessage(sender *User, evt *event.Event) { sender.Session.ChannelMessageSend(p.Key.ChannelID, content.Body) p.log.Debugln("sent message:", content.Body) } + +func (p *Portal) handleMatrixLeave(sender *User) { + if p.IsPrivateChat() { + p.log.Debugln("User left private chat portal, cleaning up and deleting...") + p.delete() + p.cleanup(false) + + return + } + + // TODO: figure out how to close a dm from the API. + + p.cleanupIfEmpty() +} + +func (p *Portal) delete() { + p.Portal.Delete() + p.bridge.portalsLock.Lock() + delete(p.bridge.portalsByID, p.Key) + + if p.MXID != "" { + delete(p.bridge.portalsByMXID, p.MXID) + } + + p.bridge.portalsLock.Unlock() +} + +func (p *Portal) cleanupIfEmpty() { + users, err := p.getMatrixUsers() + if err != nil { + p.log.Errorfln("Failed to get Matrix user list to determine if portal needs to be cleaned up: %v", err) + + return + } + + if len(users) == 0 { + p.log.Infoln("Room seems to be empty, cleaning up...") + p.delete() + p.cleanup(false) + } +} + +func (p *Portal) cleanup(puppetsOnly bool) { + if p.MXID != "" { + return + } + + if p.IsPrivateChat() { + _, err := p.MainIntent().LeaveRoom(p.MXID) + if err != nil { + p.log.Warnln("Failed to leave private chat portal with main intent:", err) + } + + return + } + + intent := p.MainIntent() + members, err := intent.JoinedMembers(p.MXID) + if err != nil { + p.log.Errorln("Failed to get portal members for cleanup:", err) + + return + } + + for member := range members.Joined { + if member == intent.UserID { + continue + } + + puppet := p.bridge.GetPuppetByMXID(member) + if p != nil { + _, err = puppet.DefaultIntent().LeaveRoom(p.MXID) + if err != nil { + p.log.Errorln("Error leaving as puppet while cleaning up portal:", err) + } + } else if !puppetsOnly { + _, err = intent.KickUser(p.MXID, &mautrix.ReqKickUser{UserID: member, Reason: "Deleting portal"}) + if err != nil { + p.log.Errorln("Error kicking user while cleaning up portal:", err) + } + } + } + + _, err = intent.LeaveRoom(p.MXID) + if err != nil { + p.log.Errorln("Error leaving with main intent while cleaning up portal:", err) + } +} + +func (p *Portal) getMatrixUsers() ([]id.UserID, error) { + members, err := p.MainIntent().JoinedMembers(p.MXID) + if err != nil { + return nil, fmt.Errorf("failed to get member list: %w", err) + } + + var users []id.UserID + for userID := range members.Joined { + _, isPuppet := p.bridge.ParsePuppetMXID(userID) + if !isPuppet && userID != p.bridge.bot.UserID { + users = append(users, userID) + } + } + + return users, nil +} + +func (p *Portal) handleMatrixKick(sender *User, target *Puppet) { + // TODO: need to learn how to make this happen as discordgo proper doesn't + // support group dms and it looks like it's a binary blob. +} diff --git a/database/portal.go b/database/portal.go index e47c0ec..ceb6a00 100644 --- a/database/portal.go +++ b/database/portal.go @@ -68,3 +68,11 @@ func (p *Portal) Update() { p.log.Warnfln("Failed to update %s: %v", p.Key, err) } } + +func (p *Portal) Delete() { + query := "DELETE FROM portal WHERE channel_id=$1 AND receiver=$2" + _, err := p.db.Exec(query, p.Key.ChannelID, p.Key.Receiver) + if err != nil { + p.log.Warnfln("Failed to delete %s: %v", p.Key, err) + } +}