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:
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
183
bridge/portal.go
183
bridge/portal.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
128
bridge/user.go
128
bridge/user.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user