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:
@@ -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
|
||||
|
||||
112
bridge/matrix.go
112
bridge/matrix.go
@@ -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
95
bridge/portal.go
Normal 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
87
bridge/puppet.go
Normal 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
93
bridge/user.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user