A ton of work getting towards dms.

This commit also points to my fork of discordgo which makes it look like the
official client which is the only way to get the actually contents of a dm
when not authorized as a bot.
This commit is contained in:
Gary Kramlich
2022-01-25 23:22:20 -06:00
parent 167fdede1f
commit 680f7bdbea
15 changed files with 478 additions and 57 deletions

View File

@@ -5,7 +5,7 @@ import (
)
func (b *Bridge) updateBotProfile() {
cfg := b.config.Appservice.Bot
cfg := b.Config.Appservice.Bot
// Set the bot's avatar.
if cfg.Avatar != "" {

View File

@@ -21,7 +21,7 @@ const (
)
type Bridge struct {
config *config.Config
Config *config.Config
log log.Logger
@@ -44,6 +44,8 @@ type Bridge struct {
puppets map[string]*Puppet
puppetsLock sync.Mutex
StateStore *database.SQLStateStore
}
func New(cfg *config.Config) (*Bridge, error) {
@@ -73,12 +75,17 @@ func New(cfg *config.Config) (*Bridge, error) {
return nil, err
}
// Create the state store
logger.Debugln("Initializing state store")
stateStore := database.NewSQLStateStore(db)
appservice.StateStore = stateStore
// Create the bridge.
bridge := &Bridge{
as: appservice,
db: db,
bot: bot,
config: cfg,
Config: cfg,
log: logger,
usersByMXID: make(map[id.UserID]*User),
@@ -88,6 +95,10 @@ func New(cfg *config.Config) (*Bridge, error) {
portalsByMXID: make(map[id.RoomID]*Portal),
portalsByID: make(map[database.PortalKey]*Portal),
puppets: make(map[string]*Puppet),
StateStore: stateStore,
}
// Setup the event processors

View File

@@ -78,7 +78,7 @@ func (mh *matrixHandler) handleMessage(evt *event.Event) {
content.RemoveReplyFallback()
if content.MsgType == event.MsgText {
prefix := mh.bridge.config.Bridge.CommandPrefix
prefix := mh.bridge.Config.Bridge.CommandPrefix
hasPrefix := strings.HasPrefix(content.Body, prefix)
if hasPrefix {
@@ -150,16 +150,16 @@ func (mh *matrixHandler) handleBotInvite(evt *event.Event) {
// 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)
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)
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.Connected)
} else {
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.config.Bridge.ManagementRoomText.NotConnected)
mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.NotConnected)
}
additionalHelp := mh.bridge.config.Bridge.ManagementRoomText.AdditionalHelp
additionalHelp := mh.bridge.Config.Bridge.ManagementRoomText.AdditionalHelp
if additionalHelp != "" {
mh.sendNoticeWithmarkdown(evt.RoomID, additionalHelp)
}
@@ -201,8 +201,6 @@ func (mh *matrixHandler) handleMembership(evt *event.Event) {
mh.handlePuppetInvite(evt, user, puppet)
}
mh.log.Warnln("no existing portal for", evt.RoomID)
return
}

View File

@@ -2,9 +2,12 @@ package bridge
import (
"fmt"
"sync"
"github.com/bwmarrin/discordgo"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@@ -12,7 +15,12 @@ import (
"gitlab.com/beeper/discord/database"
)
type PortalMatrixMessage struct {
type portalDiscordMessage struct {
msg interface{}
user *User
}
type portalMatrixMessage struct {
evt *event.Event
user *User
}
@@ -23,9 +31,18 @@ type Portal struct {
bridge *Bridge
log log.Logger
matrixMessages chan PortalMatrixMessage
channelType discordgo.ChannelType
roomCreateLock sync.Mutex
discordMessages chan portalDiscordMessage
matrixMessages chan portalMatrixMessage
}
var (
portalCreationDummyEvent = event.Type{Type: "fi.mau.dummy.portal_created", Class: event.MessageEventType}
)
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.
@@ -63,13 +80,26 @@ func (b *Bridge) GetPortalByMXID(mxid id.RoomID) *Portal {
return portal
}
func (b *Bridge) GetPortalByID(key database.PortalKey) *Portal {
b.portalsLock.Lock()
defer b.portalsLock.Unlock()
portal, ok := b.portalsByID[key]
if !ok {
return b.loadPortal(b.db.Portal.GetByID(key), &key)
}
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),
discordMessages: make(chan portalDiscordMessage, b.Config.Bridge.PortalMessageBuffer),
matrixMessages: make(chan portalMatrixMessage, b.Config.Bridge.PortalMessageBuffer),
}
go portal.messageLoop()
@@ -91,13 +121,15 @@ func (p *Portal) messageLoop() {
for {
select {
case msg := <-p.matrixMessages:
p.log.Infoln("got message", msg)
p.log.Infoln("got matrix message", msg)
case msg := <-p.discordMessages:
p.handleDiscordMessage(msg)
}
}
}
func (p *Portal) IsPrivateChat() bool {
return false
return (p.channelType == discordgo.ChannelTypeDM || p.channelType == discordgo.ChannelTypeGroupDM)
}
func (p *Portal) MainIntent() *appservice.IntentAPI {
@@ -107,3 +139,142 @@ func (p *Portal) MainIntent() *appservice.IntentAPI {
return p.bridge.bot
}
func (p *Portal) createMatrixRoom(user *User, channel *discordgo.Channel) error {
p.roomCreateLock.Lock()
defer p.roomCreateLock.Unlock()
// If we have a matrix id the room should exist so we have nothing to do.
if p.MXID != "" {
return nil
}
p.channelType = channel.Type
intent := p.MainIntent()
if err := intent.EnsureRegistered(); err != nil {
return err
}
if p.IsPrivateChat() {
puppet := p.bridge.GetPuppetByID(p.Key.ID)
puppet.SyncContact(user)
p.Name = puppet.DisplayName
p.Avatar = puppet.Avatar
p.AvatarURL = puppet.AvatarURL
}
p.log.Infoln("Creating Matrix room. Info source:", p.Portal.Key.ID)
initialState := []*event.Event{}
creationContent := make(map[string]interface{})
// if !portal.bridge.Config.Bridge.FederateRooms {
creationContent["m.federate"] = false
// }
var invite []id.UserID
if p.IsPrivateChat() {
invite = append(invite, p.bridge.bot.UserID)
}
resp, err := intent.CreateRoom(&mautrix.ReqCreateRoom{
Visibility: "private",
Name: p.Name,
Topic: p.Topic,
Preset: "private_chat",
IsDirect: p.IsPrivateChat(),
InitialState: initialState,
CreationContent: creationContent,
})
if err != nil {
return err
}
p.MXID = resp.RoomID
p.Update()
p.bridge.portalsLock.Lock()
p.bridge.portalsByMXID[p.MXID] = p
p.bridge.portalsLock.Unlock()
p.ensureUserInvited(user)
// if p.IsPrivateChat() {
// puppet := user.bridge.GetPuppetByID(p.Key.ID)
// if p.bridge.Config.Bridge.Encryption.Default {
// err = portal.bridge.Bot.EnsureJoined(portal.MXID)
// if err != nil {
// portal.log.Errorln("Failed to join created portal with bridge bot for e2be:", err)
// }
// }
// user.UpdateDirectChats(map[id.UserID][]id.RoomID{puppet.MXID: {portal.MXID}})
// }
firstEventResp, err := p.MainIntent().SendMessageEvent(p.MXID, portalCreationDummyEvent, struct{}{})
if err != nil {
p.log.Errorln("Failed to send dummy event to mark portal creation:", err)
} else {
p.FirstEventID = firstEventResp.EventID
p.Update()
}
return nil
}
func (p *Portal) handleDiscordMessage(msg portalDiscordMessage) {
if p.MXID == "" {
p.log.Debugln("Creating Matrix room from incoming message")
discordMsg := msg.msg.(*discordgo.MessageCreate)
channel, err := msg.user.Session.Channel(discordMsg.ChannelID)
if err != nil {
p.log.Errorln("Failed to find channel for message:", err)
return
}
if err := p.createMatrixRoom(msg.user, channel); err != nil {
p.log.Errorln("Failed to create portal room:", err)
return
}
}
switch msg.msg.(type) {
case *discordgo.MessageCreate:
p.handleMessage(msg.msg.(*discordgo.MessageCreate).Message)
default:
p.log.Warnln("unknown message type")
}
}
func (p *Portal) ensureUserInvited(user *User) bool {
return user.ensureInvited(p.MainIntent(), p.MXID, p.IsPrivateChat())
}
func (p *Portal) handleMessage(msg *discordgo.Message) {
if p.MXID == "" {
p.log.Warnln("handle message called without a valid portal")
return
}
// TODO: Check if we already got the message
p.log.Debugln("content", msg.Content)
p.log.Debugln("embeds", msg.Embeds)
p.log.Debugln("msg", msg)
content := &event.MessageEventContent{
Body: msg.Content,
MsgType: event.MsgText,
}
resp, err := p.MainIntent().SendMessageEvent(p.MXID, event.EventMessage, content)
p.log.Warnln("response:", resp)
p.log.Warnln("error:", err)
}

View File

@@ -3,6 +3,7 @@ package bridge
import (
"fmt"
"regexp"
"sync"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/appservice"
@@ -18,6 +19,8 @@ type Puppet struct {
log log.Logger
MXID id.UserID
syncLock sync.Mutex
}
var userIDRegex *regexp.Regexp
@@ -36,8 +39,8 @@ 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,
b.Config.Bridge.FormatUsername("([0-9]+)"),
b.Config.Homeserver.Domain,
)
userIDRegex = regexp.MustCompile(pattern)
@@ -82,11 +85,27 @@ func (b *Bridge) GetPuppetByID(id string) *Puppet {
func (b *Bridge) FormatPuppetMXID(did string) id.UserID {
return id.NewUserID(
b.config.Bridge.FormatUsername(did),
b.config.Homeserver.Domain,
b.Config.Bridge.FormatUsername(did),
b.Config.Homeserver.Domain,
)
}
func (p *Puppet) DefaultIntent() *appservice.IntentAPI {
return p.bridge.as.Intent(p.MXID)
}
func (p *Puppet) SyncContact(user *User) {
p.syncLock.Lock()
defer p.syncLock.Unlock()
dUser, err := user.Session.User(p.ID)
if err != nil {
p.log.Warnfln("failed to sync puppet %s: %v", p.ID, err)
return
}
p.DisplayName = p.bridge.Config.Bridge.FormatDisplayname(dUser)
p.Update()
}

View File

@@ -1,10 +1,14 @@
package bridge
import (
"errors"
"strings"
"github.com/bwmarrin/discordgo"
"github.com/skip2/go-qrcode"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@@ -177,13 +181,131 @@ func (u *User) Login(token string) error {
}
func (u *User) Connect() error {
u.log.Debugln("connecting to discord")
// get our user info
user, err := u.User.Session.User("@me")
if err != nil {
return err
}
u.User.ID = user.ID
// Add our event handlers
u.User.Session.AddHandler(u.connectedHandler)
u.User.Session.AddHandler(u.disconnectedHandler)
u.User.Session.AddHandler(u.channelCreateHandler)
u.User.Session.AddHandler(u.channelDeleteHandler)
u.User.Session.AddHandler(u.channelPinsUpdateHandler)
u.User.Session.AddHandler(u.channelUpdateHandler)
u.User.Session.AddHandler(u.messageHandler)
u.log.Warnln("logged in, opening websocket")
// u.User.Session.Identify.Capabilities = 125
// // Setup our properties
// u.User.Session.Identify.Properties = discordgo.IdentifyProperties{
// OS: "Windows",
// OSVersion: "10",
// Browser: "Chrome",
// BrowserUserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36",
// BrowserVersion: "92.0.4515.159",
// Referrer: "https://discord.com/channels/@me",
// ReferringDomain: "discord.com",
// ClientBuildNumber: "83364",
// ReleaseChannel: "stable",
// }
u.User.Session.Identify.Presence.Status = "online"
return u.User.Session.Open()
}
func (u *User) messageHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
u.log.Warnln("received message", m)
func (u *User) connectedHandler(s *discordgo.Session, c *discordgo.Connect) {
u.log.Debugln("connected to discord")
}
func (u *User) disconnectedHandler(s *discordgo.Session, d *discordgo.Disconnect) {
u.log.Debugln("disconnected from discord")
}
func (u *User) channelCreateHandler(s *discordgo.Session, c *discordgo.ChannelCreate) {
key := database.NewPortalKey(u.User.ID, c.ID)
portal := u.bridge.GetPortalByID(key)
portal.Name = c.Name
portal.Topic = c.Topic
if c.Icon != "" {
u.log.Debugln("channel icon", c.Icon)
}
portal.Update()
portal.createMatrixRoom(u, c.Channel)
}
func (u *User) channelDeleteHandler(s *discordgo.Session, c *discordgo.ChannelDelete) {
u.log.Debugln("channel delete handler")
}
func (u *User) channelPinsUpdateHandler(s *discordgo.Session, c *discordgo.ChannelPinsUpdate) {
u.log.Debugln("channel pins update")
}
func (u *User) channelUpdateHandler(s *discordgo.Session, c *discordgo.ChannelUpdate) {
key := database.NewPortalKey(u.User.ID, c.ID)
portal := u.bridge.GetPortalByID(key)
portal.Name = c.Name
portal.Topic = c.Topic
u.log.Debugln("channel icon", c.Icon)
portal.Update()
u.log.Debugln("channel update")
}
func (u *User) messageHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
if m.GuildID != "" {
u.log.Debugln("ignoring guild build messaged")
return
}
key := database.NewPortalKey(u.User.ID, m.ChannelID)
portal := u.bridge.GetPortalByID(key)
msg := portalDiscordMessage{
msg: m,
user: u,
}
portal.discordMessages <- msg
}
func (u *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isDirect bool) bool {
ret := false
inviteContent := event.Content{
Parsed: &event.MemberEventContent{
Membership: event.MembershipInvite,
IsDirect: isDirect,
},
Raw: map[string]interface{}{},
}
resp, err := intent.SendStateEvent(roomID, event.StateMember, u.MXID.String(), &inviteContent)
u.log.Warnfln("resp: %#v", resp)
var httpErr mautrix.HTTPError
if err != nil && errors.As(err, &httpErr) && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
u.bridge.StateStore.SetMembership(roomID, u.MXID, event.MembershipJoin)
ret = true
} else if err != nil {
u.log.Warnfln("Failed to invite user to %s: %v", roomID, err)
} else {
ret = true
}
return ret
}