Initial bot functionality

* The bot now properly joins the management room
* The management room is persisted in the database
* Welcome/help messages are sent in the management room
This commit is contained in:
Gary Kramlich
2021-12-30 09:33:06 -06:00
parent 78ab3d3804
commit 456a15ba56
19 changed files with 859 additions and 16 deletions

View File

@@ -3,11 +3,13 @@ package bridge
import (
"errors"
"fmt"
"sync"
"time"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/id"
"gitlab.com/beeper/discord/config"
"gitlab.com/beeper/discord/database"
@@ -28,6 +30,20 @@ type Bridge struct {
eventProcessor *appservice.EventProcessor
matrixHandler *matrixHandler
bot *appservice.IntentAPI
usersByMXID map[id.UserID]*User
usersByID map[string]*User
usersLock sync.Mutex
managementRooms map[id.RoomID]*User
managementRoomsLock sync.Mutex
portalsByMXID map[id.RoomID]*Portal
portalsByID map[database.PortalKey]*Portal
portalsLock sync.Mutex
puppets map[string]*Puppet
puppetsLock sync.Mutex
}
func New(cfg *config.Config) (*Bridge, error) {
@@ -64,6 +80,14 @@ func New(cfg *config.Config) (*Bridge, error) {
bot: bot,
config: cfg,
log: logger,
usersByMXID: make(map[id.UserID]*User),
usersByID: make(map[string]*User),
managementRooms: make(map[id.RoomID]*User),
portalsByMXID: make(map[id.RoomID]*Portal),
portalsByID: make(map[database.PortalKey]*Portal),
}
// Setup the event processors

View File

@@ -5,6 +5,7 @@ import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
@@ -60,16 +61,92 @@ func (mh *matrixHandler) ignoreEvent(evt *event.Event) bool {
}
func (mh *matrixHandler) handleMessage(evt *event.Event) {
mh.log.Debugfln("received message from %q: %q", evt.Sender, evt.Content.AsMessage())
if mh.ignoreEvent(evt) {
return
}
mh.log.Debugfln("received message from %q: %q", evt.Sender, evt.Content.AsMessage())
}
func (mh *matrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers {
resp, err := intent.JoinRoomByID(evt.RoomID)
if err != nil {
mh.log.Debugfln("Failed to join room %q as %q with invite from %q: %v", evt.RoomID, intent.UserID, evt.Sender, err)
return nil
}
members, err := intent.JoinedMembers(resp.RoomID)
if err != nil {
mh.log.Debugfln("Failed to get members in room %q with invite from %q as %q: %v", resp.RoomID, evt.Sender, intent.UserID, err)
return nil
}
if len(members.Joined) < 2 {
mh.log.Debugfln("Leaving empty room %q with invite from %q as %q", resp.RoomID, evt.Sender, intent.UserID)
intent.LeaveRoom(resp.RoomID)
return nil
}
return members
}
func (mh *matrixHandler) sendNoticeWithmarkdown(roomID id.RoomID, message string) (*mautrix.RespSendEvent, error) {
intent := mh.as.BotIntent()
content := format.RenderMarkdown(message, true, false)
content.MsgType = event.MsgNotice
return intent.SendMessageEvent(roomID, event.EventMessage, content)
}
func (mh *matrixHandler) handleBotInvite(evt *event.Event) {
intent := mh.as.BotIntent()
user := mh.bridge.GetUserByMXID(evt.Sender)
if user == nil {
return
}
members := mh.joinAndCheckMembers(evt, intent)
if members == nil {
return
}
// If this is a DM and the user doesn't have a management room, make this
// the management room.
if len(members.Joined) == 2 && (user.ManagementRoom == "" || evt.Content.AsMember().IsDirect) {
user.SetManagementRoom(evt.RoomID)
intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room")
mh.log.Debugfln("%q registered as management room with %q", evt.RoomID, evt.Sender)
}
// Wait to send the welcome message until we're sure we're not in an empty
// room.
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.config.Bridge.ManagementRoomText.Welcome)
if evt.RoomID == user.ManagementRoom {
if user.HasSession() {
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.config.Bridge.ManagementRoomText.Connected)
} else {
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.config.Bridge.ManagementRoomText.NotConnected)
}
additionalHelp := mh.bridge.config.Bridge.ManagementRoomText.AdditionalHelp
if additionalHelp != "" {
mh.sendNoticeWithmarkdown(evt.RoomID, additionalHelp)
}
}
}
func (mh *matrixHandler) handlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) {
mh.log.Warnln("handling puppet invite!")
}
func (mh *matrixHandler) handleMembership(evt *event.Event) {
mh.log.Debugfln("recevied invite %#v\n", evt)
// Return early if we're supposed to ignore the event.
if mh.ignoreEvent(evt) {
return
@@ -78,12 +155,37 @@ func (mh *matrixHandler) handleMembership(evt *event.Event) {
// Grab the content of the event.
content := evt.Content.AsMember()
// TODO: handle invites from ourselfs?
// Check if this is a new conversation from a matrix user to the bot
if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mh.as.BotMXID() {
mh.handleBotInvite(evt)
return
}
// Load or create a new user.
user := mh.bridge.GetUserByMXID(evt.Sender)
if user == nil {
return
}
// 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)
}
mh.log.Warnln("no existing portal for", evt.RoomID)
return
}
isSelf := id.UserID(evt.GetStateKey()) == evt.Sender
// Handle matrix invites.
if content.Membership == event.MembershipInvite && !isSelf {
//
portal.HandleMatrixInvite(user, evt)
}
}

95
bridge/portal.go Normal file
View File

@@ -0,0 +1,95 @@
package bridge
import (
"fmt"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"gitlab.com/beeper/discord/database"
)
type PortalMatrixMessage struct {
evt *event.Event
user *User
}
type Portal struct {
*database.Portal
bridge *Bridge
log log.Logger
matrixMessages chan PortalMatrixMessage
}
func (b *Bridge) loadPortal(dbPortal *database.Portal, key *database.PortalKey) *Portal {
// If we weren't given a portal we'll attempt to create it if a key was
// provided.
if dbPortal == nil {
if key == nil {
return nil
}
dbPortal = b.db.Portal.New()
dbPortal.Key = *key
dbPortal.Insert()
}
portal := b.NewPortal(dbPortal)
// No need to lock, it is assumed that our callers have already acquired
// the lock.
b.portalsByID[portal.Key] = portal
if portal.MXID != "" {
b.portalsByMXID[portal.MXID] = portal
}
return portal
}
func (b *Bridge) GetPortalByMXID(mxid id.RoomID) *Portal {
b.portalsLock.Lock()
defer b.portalsLock.Unlock()
portal, ok := b.portalsByMXID[mxid]
if !ok {
return b.loadPortal(b.db.Portal.GetByMXID(mxid), nil)
}
return portal
}
func (b *Bridge) NewPortal(dbPortal *database.Portal) *Portal {
portal := &Portal{
Portal: dbPortal,
bridge: b,
log: b.log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)),
matrixMessages: make(chan PortalMatrixMessage, b.config.Bridge.PortalMessageBuffer),
}
go portal.messageLoop()
return portal
}
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 {
p.log.Infoln("no puppet for %v", sender)
// Open a conversation on the discord side?
}
p.log.Infoln("puppet:", puppet)
}
func (p *Portal) messageLoop() {
for {
select {
case msg := <-p.matrixMessages:
p.log.Infoln("got message", msg)
}
}
}

87
bridge/puppet.go Normal file
View File

@@ -0,0 +1,87 @@
package bridge
import (
"fmt"
"regexp"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"gitlab.com/beeper/discord/database"
)
type Puppet struct {
*database.Puppet
bridge *Bridge
log log.Logger
MXID id.UserID
}
var userIDRegex *regexp.Regexp
func (b *Bridge) NewPuppet(dbPuppet *database.Puppet) *Puppet {
return &Puppet{
Puppet: dbPuppet,
bridge: b,
log: b.log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.ID)),
MXID: b.FormatPuppetMXID(dbPuppet.ID),
}
}
func (b *Bridge) ParsePuppetMXID(mxid id.UserID) (string, bool) {
if userIDRegex == nil {
pattern := fmt.Sprintf(
"^@%s:%s$",
b.config.Bridge.FormatUsername("([0-9]+)"),
b.config.Homeserver.Domain,
)
userIDRegex = regexp.MustCompile(pattern)
}
match := userIDRegex.FindStringSubmatch(string(mxid))
if len(match) == 2 {
return match[1], true
}
return "", false
}
func (b *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
id, ok := b.ParsePuppetMXID(mxid)
if !ok {
return nil
}
return b.GetPuppetByID(id)
}
func (b *Bridge) GetPuppetByID(id string) *Puppet {
b.puppetsLock.Lock()
defer b.puppetsLock.Unlock()
puppet, ok := b.puppets[id]
if !ok {
dbPuppet := b.db.Puppet.Get(id)
if dbPuppet == nil {
dbPuppet = b.db.Puppet.New()
dbPuppet.ID = id
dbPuppet.Insert()
}
puppet = b.NewPuppet(dbPuppet)
b.puppets[puppet.ID] = puppet
}
return puppet
}
func (b *Bridge) FormatPuppetMXID(did string) id.UserID {
return id.NewUserID(
b.config.Bridge.FormatUsername(did),
b.config.Homeserver.Domain,
)
}

93
bridge/user.go Normal file
View File

@@ -0,0 +1,93 @@
package bridge
import (
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"gitlab.com/beeper/discord/database"
)
type User struct {
*database.User
bridge *Bridge
log log.Logger
}
func (b *Bridge) loadUser(dbUser *database.User, mxid *id.UserID) *User {
// If we weren't passed in a user we attempt to create one if we were given
// a matrix id.
if dbUser == nil {
if mxid == nil {
return nil
}
dbUser = b.db.User.New()
dbUser.MXID = *mxid
dbUser.Insert()
}
user := b.NewUser(dbUser)
// We assume the usersLock was acquired by our caller.
b.usersByMXID[user.MXID] = user
if user.ID != "" {
b.usersByID[user.ID] = user
}
if user.ManagementRoom != "" {
// Lock the management rooms for our update
b.managementRoomsLock.Lock()
b.managementRooms[user.ManagementRoom] = user
b.managementRoomsLock.Unlock()
}
return user
}
func (b *Bridge) GetUserByMXID(userID id.UserID) *User {
// TODO: check if puppet
b.usersLock.Lock()
defer b.usersLock.Unlock()
user, ok := b.usersByMXID[userID]
if !ok {
return b.loadUser(b.db.User.GetByMXID(userID), &userID)
}
return user
}
func (b *Bridge) NewUser(dbUser *database.User) *User {
user := &User{
User: dbUser,
bridge: b,
log: b.log.Sub("User").Sub(string(dbUser.MXID)),
}
return user
}
func (u *User) SetManagementRoom(roomID id.RoomID) {
u.bridge.managementRoomsLock.Lock()
defer u.bridge.managementRoomsLock.Unlock()
existing, ok := u.bridge.managementRooms[roomID]
if ok {
// If there's a user already assigned to this management room, clear it
// out.
// I think this is due a name change or something? I dunno, leaving it
// for now.
existing.ManagementRoom = ""
existing.Update()
}
u.ManagementRoom = roomID
u.bridge.managementRooms[u.ManagementRoom] = u
u.Update()
}
func (u *User) HasSession() bool {
return u.User.Session != nil
}