From 4e41c2f2270e737f9363bf551e6714f63dc17e6c Mon Sep 17 00:00:00 2001 From: Skip R Date: Mon, 24 Nov 2025 12:41:40 -0800 Subject: [PATCH] sync private channels and their members --- pkg/connector/client.go | 61 ++++++++++++++++++++++++++++++++++-- pkg/connector/events.go | 65 +++++++++++++++++++++++++++++++++++++++ pkg/connector/id.go | 30 ++++++++++++++++++ pkg/connector/userinfo.go | 25 +++++++++++++-- 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 pkg/connector/events.go create mode 100644 pkg/connector/id.go diff --git a/pkg/connector/client.go b/pkg/connector/client.go index b65cdb4..b4a2099 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -25,12 +25,16 @@ import ( "github.com/bwmarrin/discordgo" "github.com/rs/zerolog" "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/database" + "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/status" ) type DiscordClient struct { - UserLogin *bridgev2.UserLogin - Session *discordgo.Session + connector *DiscordConnector + usersFromReady map[string]*discordgo.User + UserLogin *bridgev2.UserLogin + Session *discordgo.Session } func (d *DiscordConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserLogin) error { @@ -54,6 +58,7 @@ func (d *DiscordConnector) LoadUserLogin(ctx context.Context, login *bridgev2.Us session.EventHandler = func(evt any) {} login.Client = &DiscordClient{ + connector: d, UserLogin: login, Session: session, } @@ -126,6 +131,15 @@ func (cl *DiscordClient) connect(ctx context.Context) error { } } + // Stash all of the users we received in READY so we can perform quick lookups + // keyed by user ID. + cl.usersFromReady = make(map[string]*discordgo.User) + for _, user := range cl.Session.State.Ready.Users { + cl.usersFromReady[user.ID] = user + } + + go cl.syncChannels(ctx) + return nil } @@ -143,3 +157,46 @@ func (d *DiscordClient) LogoutRemote(ctx context.Context) { // FIXME(skip): Implement. d.Disconnect() } + +func (d *DiscordClient) syncChannels(ctx context.Context) { + for _, dm := range d.Session.State.PrivateChannels { + d.UserLogin.Log.Debug().Str("channel_id", dm.ID).Msg("Syncing private channel") + d.syncChannel(ctx, dm) + } +} + +func (d *DiscordClient) syncChannel(ctx context.Context, ch *discordgo.Channel) { + isGroup := len(ch.RecipientIDs) > 1 + + var roomType database.RoomType + if isGroup { + roomType = database.RoomTypeGroupDM + } else { + roomType = database.RoomTypeDM + } + + var members bridgev2.ChatMemberList + members.IsFull = true + members.MemberMap = make(bridgev2.ChatMemberMap, len(ch.Recipients)) + if len(ch.Recipients) > 0 { + for _, recipient := range ch.Recipients { + sender := bridgev2.EventSender{ + IsFromMe: recipient.ID == d.Session.State.User.ID, + SenderLogin: d.UserLogin.ID, + Sender: networkid.UserID(recipient.ID), + } + members.MemberMap[sender.Sender] = bridgev2.ChatMember{EventSender: sender} + } + members.TotalMemberCount = len(ch.Recipients) + } + + d.connector.bridge.QueueRemoteEvent(d.UserLogin, &DiscordChatResync{ + channel: ch, + portalKey: d.makePortalKey(ch, d.UserLogin.ID, true), + info: &bridgev2.ChatInfo{ + Name: &ch.Name, + Members: &members, + Type: &roomType, + }, + }) +} diff --git a/pkg/connector/events.go b/pkg/connector/events.go new file mode 100644 index 0000000..ad4cca9 --- /dev/null +++ b/pkg/connector/events.go @@ -0,0 +1,65 @@ +// mautrix-discord - A Matrix-Discord puppeting bridge. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "context" + + "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/networkid" +) + +type DiscordChatResync struct { + channel *discordgo.Channel + portalKey networkid.PortalKey + info *bridgev2.ChatInfo +} + +var ( + _ bridgev2.RemoteChatResyncWithInfo = (*DiscordChatResync)(nil) + _ bridgev2.RemoteEventThatMayCreatePortal = (*DiscordChatResync)(nil) +) + +func (d *DiscordChatResync) AddLogContext(c zerolog.Context) zerolog.Context { + c = c.Str("channel_id", d.channel.ID).Int("channel_type", int(d.channel.Type)) + return c +} + +func (d *DiscordChatResync) GetPortalKey() networkid.PortalKey { + return d.portalKey +} + +func (d *DiscordChatResync) GetSender() bridgev2.EventSender { + return bridgev2.EventSender{} +} + +func (d *DiscordChatResync) GetType() bridgev2.RemoteEventType { + return bridgev2.RemoteEventChatResync +} + +func (d *DiscordChatResync) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) { + if d.info == nil { + return nil, nil + } + return d.info, nil +} + +func (d *DiscordChatResync) ShouldCreatePortal() bool { + return true +} diff --git a/pkg/connector/id.go b/pkg/connector/id.go new file mode 100644 index 0000000..7376061 --- /dev/null +++ b/pkg/connector/id.go @@ -0,0 +1,30 @@ +// mautrix-discord - A Matrix-Discord puppeting bridge. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "github.com/bwmarrin/discordgo" + "maunium.net/go/mautrix/bridgev2/networkid" +) + +func (d *DiscordClient) makePortalKey(ch *discordgo.Channel, userLoginID networkid.UserLoginID, wantReceiver bool) (key networkid.PortalKey) { + key.ID = networkid.PortalID(ch.ID) + if wantReceiver { + key.Receiver = userLoginID + } + return +} diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index bedb53f..bd09c1c 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -18,7 +18,10 @@ package connector import ( "context" + "fmt" + "github.com/rs/zerolog" + "go.mau.fi/util/ptr" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" ) @@ -30,6 +33,24 @@ func (d *DiscordClient) IsThisUser(ctx context.Context, userID networkid.UserID) } func (d *DiscordClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) { - //TODO implement me - panic("implement me") + log := zerolog.Ctx(ctx) + + if ghost.ID == "" { + log.Warn().Msg("Tried to get user info for ghost with no ID") + return nil, nil + } + + // FIXME(skip): This won't work for users in guilds. + + user, ok := d.usersFromReady[string(ghost.ID)] + if !ok { + log.Error().Str("ghost_id", string(ghost.ID)).Msg("Couldn't find corresponding user from READY for ghost") + return nil, nil + } + + return &bridgev2.UserInfo{ + Identifiers: []string{fmt.Sprintf("discord:%s", user.ID)}, + Name: ptr.Ptr(user.DisplayName()), + IsBot: &user.Bot, + }, nil }