diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 67e699b9..1ab5499f 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -549,27 +549,6 @@ func (t *TelegramClient) Disconnect() { t.userLogin.Log.Info().Msg("Disconnect complete") } -func (t *TelegramClient) getSingleChannel(ctx context.Context, id int64) (*tg.Channel, error) { - accessHash, err := t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeChannel, id) - if err != nil { - return nil, err - } - chats, err := APICallWithOnlyChatUpdates(ctx, t, func() (tg.MessagesChatsClass, error) { - return t.client.API().ChannelsGetChannels(ctx, []tg.InputChannelClass{ - &tg.InputChannel{ChannelID: id, AccessHash: accessHash}, - }) - }) - if err != nil { - return nil, err - } else if len(chats.GetChats()) == 0 { - return nil, fmt.Errorf("failed to get channel info for channel %d", id) - } else if channel, ok := chats.GetChats()[0].(*tg.Channel); !ok { - return nil, fmt.Errorf("unexpected channel type %T", chats.GetChats()[id]) - } else { - return channel, nil - } -} - func (t *TelegramClient) IsLoggedIn() bool { // TODO use less hacky check than context cancellation return t != nil && t.client != nil && diff --git a/pkg/connector/tomatrix.go b/pkg/connector/tomatrix.go index 7272a19a..501350cd 100644 --- a/pkg/connector/tomatrix.go +++ b/pkg/connector/tomatrix.go @@ -17,6 +17,7 @@ package connector import ( + "cmp" "context" "crypto/sha256" "encoding/binary" @@ -209,6 +210,12 @@ func (c *TelegramClient) convertToMatrix( ContentHash: hasher.Sum(nil), ContentURI: contentURI, } + if fwd, isForwarded := msg.GetFwdFrom(); isForwarded { + err = c.addForwardHeader(ctx, cm.Parts[0], fwd) + if err != nil { + return nil, fmt.Errorf("failed to add forward header: %w", err) + } + } if replyTo, ok := msg.GetReplyTo(); ok { switch replyTo := replyTo.(type) { @@ -236,6 +243,99 @@ func (c *TelegramClient) convertToMatrix( return } +func (t *TelegramClient) addForwardHeader(ctx context.Context, part *bridgev2.ConvertedMessagePart, fwd tg.MessageFwdHeader) error { + var fwdFromText, fwdFromHTML string + switch from := fwd.FromID.(type) { + case *tg.PeerUser: + user := t.main.Bridge.GetCachedUserLoginByID(ids.MakeUserLoginID(from.UserID)) + var mxid id.UserID + if user != nil { + mxid = user.UserMXID + fwdFromText = cmp.Or(user.RemoteName, user.UserMXID.String()) + } else if ghost, err := t.main.Bridge.GetGhostByID(ctx, ids.MakeUserID(from.UserID)); err != nil { + return err + } else { + if ghost.Name == "" { + info, err := t.GetUserInfo(ctx, ghost) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get user info to add forward header") + } else if info != nil { + ghost.UpdateInfo(ctx, info) + } + } + mxid = ghost.Intent.GetMXID() + fwdFromText = cmp.Or(ghost.Name, fwd.FromName, "unknown user") + } + fwdFromHTML = fmt.Sprintf( + `%s`, + mxid.URI().MatrixToURL(), + html.EscapeString(fwdFromText), + ) + case *tg.PeerChannel, *tg.PeerChat: + unknownType := "unknown chat" + if _, ok := from.(*tg.PeerChannel); ok { + unknownType = "unknown channel" + } + portal, err := t.main.Bridge.GetExistingPortalByKey(ctx, t.makePortalKeyFromPeer(from, 0)) + if err != nil { + return err + } else if portal != nil && portal.MXID != "" { + fwdFromText = cmp.Or(portal.Name, fwd.FromName, unknownType) + fwdFromHTML = fmt.Sprintf( + `%s`, + portal.MXID.URI().MatrixToURL(), + html.EscapeString(fwdFromText), + ) + } else if fwd.FromName != "" { + fwdFromText = fwd.FromName + fwdFromHTML = fmt.Sprintf("%s", html.EscapeString(fwd.FromName)) + } else { + fwdFromText = unknownType + fwdFromHTML = unknownType + } + // TODO fetch channel if not found + } + if fwdFromText == "" && fwd.FromName != "" { + fwdFromText = fwd.FromName + fwdFromHTML = fmt.Sprintf("%s", html.EscapeString(fwd.FromName)) + } + if fwdFromText == "" { + fwdFromText = "unknown source" + fwdFromHTML = fwdFromText + } + + if part.Content.MsgType.IsMedia() { + if part.Content.FileName == "" { + part.Content.FileName = part.Content.Body + } + if part.Content.Body == part.Content.FileName { + part.Content.Body = "" + } + } + + part.Content.EnsureHasHTML() + existingBodyLines := strings.Split(part.Content.Body, "\n") + for i, line := range existingBodyLines { + existingBodyLines[i] = fmt.Sprintf("> %s", line) + } + if len(existingBodyLines) > 0 { + existingBodyLines = append([]string{"\n"}, existingBodyLines...) + } + part.Content.Body = fmt.Sprintf( + "Forwarded message from %s%s", + fwdFromText, strings.Join(existingBodyLines, "\n"), + ) + existingFormattedBody := part.Content.FormattedBody + if existingFormattedBody != "" { + existingFormattedBody = fmt.Sprintf("
%s
", existingFormattedBody) + } + part.Content.FormattedBody = fmt.Sprintf( + "Forwarded message from %s%s", + fwdFromHTML, existingFormattedBody, + ) + return nil +} + func (t *TelegramClient) parseBodyAndHTML(ctx context.Context, message string, entities []tg.MessageEntityClass) *event.MessageEventContent { if len(entities) == 0 { return &event.MessageEventContent{MsgType: event.MsgText, Body: message} diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index e99f40ed..6f5f9b53 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -40,33 +40,64 @@ func (t *TelegramClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) } } +func (t *TelegramClient) getChatPeerForInputFromMessage(ctx context.Context, id int64, peerID tg.PeerClass) (tg.InputPeerClass, error) { + switch typedChat := peerID.(type) { + case *tg.PeerUser: + if id == typedChat.UserID { + // We don't have the user's access hash + return nil, nil + } + accessHash, err := t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeUser, typedChat.UserID) + if err != nil { + return nil, err + } + return &tg.InputPeerUser{UserID: typedChat.UserID, AccessHash: accessHash}, nil + case *tg.PeerChat: + return &tg.InputPeerChat{ChatID: typedChat.ChatID}, nil + case *tg.PeerChannel: + if id == typedChat.ChannelID { + // We don't have the channel's access hash + return nil, nil + } + accessHash, err := t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeChannel, typedChat.ChannelID) + if err != nil { + return nil, err + } + return &tg.InputPeerChannel{ChannelID: typedChat.ChannelID, AccessHash: accessHash}, nil + default: + return nil, nil + } +} + func (t *TelegramClient) getInputUserFromContext(ctx context.Context, id int64) (*tg.InputUserFromMessage, error) { msg, ok := bridgev2.GetRemoteEventFromContext(ctx).(*simplevent.Message[*tg.Message]) if !ok { return nil, nil } - fromUser, ok := msg.Data.FromID.(*tg.PeerUser) - if !ok || fromUser.UserID != id { - return nil, nil - } - var inputPeer tg.InputPeerClass - switch typedChat := msg.Data.PeerID.(type) { - case *tg.PeerUser: - // We don't have the user's access hash - return nil, nil - case *tg.PeerChat: - inputPeer = &tg.InputPeerChat{ChatID: typedChat.ChatID} - case *tg.PeerChannel: - accessHash, err := t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeChannel, typedChat.ChannelID) - if err != nil { - return nil, err - } - inputPeer = &tg.InputPeerChannel{ChannelID: typedChat.ChannelID, AccessHash: accessHash} + inputPeer, err := t.getChatPeerForInputFromMessage(ctx, id, msg.Data.PeerID) + if err != nil || inputPeer == nil { + return nil, err } return &tg.InputUserFromMessage{ Peer: inputPeer, MsgID: msg.Data.ID, - UserID: fromUser.UserID, + UserID: id, + }, nil +} + +func (t *TelegramClient) getInputChannelFromContext(ctx context.Context, id int64) (*tg.InputChannelFromMessage, error) { + msg, ok := bridgev2.GetRemoteEventFromContext(ctx).(*simplevent.Message[*tg.Message]) + if !ok { + return nil, nil + } + inputPeer, err := t.getChatPeerForInputFromMessage(ctx, id, msg.Data.PeerID) + if err != nil || inputPeer == nil { + return nil, err + } + return &tg.InputChannelFromMessage{ + Peer: inputPeer, + MsgID: msg.Data.ID, + ChannelID: id, }, nil } @@ -112,6 +143,42 @@ func (t *TelegramClient) getSingleUser(ctx context.Context, id int64) (tg.UserCl } } +func (t *TelegramClient) getInputChannel(ctx context.Context, id int64) (tg.InputChannelClass, error) { + accessHash, err := t.ScopedStore.GetAccessHash(ctx, ids.PeerTypeChannel, id) + if err != nil { + fromMsg, fromMsgErr := t.getInputChannelFromContext(ctx, id) + if fromMsgErr != nil { + return nil, fmt.Errorf("%w, also failed to get from message: %w", err, fromMsgErr) + } else if fromMsg == nil { + return nil, err + } + zerolog.Ctx(ctx).Trace(). + Any("input_peer", fromMsg). + Msg("Using InputChannelFromMessage as access hash wasn't found") + return fromMsg, nil + } + return &tg.InputChannel{ChannelID: id, AccessHash: accessHash}, nil +} + +func (t *TelegramClient) getSingleChannel(ctx context.Context, id int64) (*tg.Channel, error) { + inputChannel, err := t.getInputChannel(ctx, id) + if err != nil { + return nil, err + } + chats, err := APICallWithOnlyChatUpdates(ctx, t, func() (tg.MessagesChatsClass, error) { + return t.client.API().ChannelsGetChannels(ctx, []tg.InputChannelClass{inputChannel}) + }) + if err != nil { + return nil, err + } else if len(chats.GetChats()) == 0 { + return nil, fmt.Errorf("failed to get channel info for channel %d", id) + } else if channel, ok := chats.GetChats()[0].(*tg.Channel); !ok { + return nil, fmt.Errorf("unexpected channel type %T", chats.GetChats()[id]) + } else { + return channel, nil + } +} + func (t *TelegramClient) wrapChannelGhostInfo(ctx context.Context, channel *tg.Channel) (*bridgev2.UserInfo, error) { var err error if accessHash, ok := channel.GetAccessHash(); ok && !channel.Min {