Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccb349f3d2 | |||
| 4768065e72 | |||
| 64bf6bfe90 | |||
| aab48f0dbe | |||
| b00e2d8955 | |||
| e3bb26aee1 | |||
| 7c2c72bbde | |||
| 2ffbde7448 | |||
| 2a0da7801a | |||
| eaf387abfe | |||
| 64d80c3d1d | |||
| c78b1abd2d | |||
| 12f900a7bd | |||
| cdb77f938a | |||
| 5a1a478992 | |||
| d2a06ebbbe | |||
| e6243d8935 | |||
| 9e1c42a992 | |||
| 6eacf38d74 | |||
| 65fcf712d3 | |||
| 8512cfe6a6 | |||
| 7a6d1bf17a | |||
| 18f831553d | |||
| dce0c4dbe1 | |||
| ac2a2c2980 |
@@ -11,7 +11,8 @@ type: Bug
|
|||||||
|
|
||||||
### Checklist
|
### Checklist
|
||||||
|
|
||||||
<!-- Both items below are mandatory. Issues not following the rules may be closed without comment. -->
|
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
|
||||||
|
|
||||||
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
|
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
|
||||||
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
|
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
|
||||||
|
* [ ] The bug is still present on the main branch.
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
# unreleased
|
||||||
|
|
||||||
|
* Added support for bridging message reactions from Telegram when logged in as
|
||||||
|
a bot.
|
||||||
|
* Fixed `mx_room_state` table not being migrated correctly from the Python
|
||||||
|
bridge in SQLite databases.
|
||||||
|
|
||||||
# v26.04
|
# v26.04
|
||||||
|
|
||||||
* Rewrote bridge in Go using bridgev2 architecture.
|
* Rewrote bridge in Go using bridgev2 architecture.
|
||||||
|
|||||||
@@ -262,6 +262,9 @@ CREATE TABLE new_mx_room_state (
|
|||||||
INSERT INTO new_mx_room_state (room_id, encryption, power_levels, create_event, members_fetched)
|
INSERT INTO new_mx_room_state (room_id, encryption, power_levels, create_event, members_fetched)
|
||||||
SELECT room_id, encryption, power_levels, create_event, COALESCE(has_full_member_list, false)
|
SELECT room_id, encryption, power_levels, create_event, COALESCE(has_full_member_list, false)
|
||||||
FROM mx_room_state;
|
FROM mx_room_state;
|
||||||
|
|
||||||
|
DROP TABLE mx_room_state;
|
||||||
|
ALTER TABLE new_mx_room_state RENAME TO mx_room_state;
|
||||||
-- end only sqlite
|
-- end only sqlite
|
||||||
|
|
||||||
ALTER TABLE mx_user_profile ADD COLUMN name_skeleton bytea;
|
ALTER TABLE mx_user_profile ADD COLUMN name_skeleton bytea;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ require (
|
|||||||
github.com/rs/zerolog v1.35.0
|
github.com/rs/zerolog v1.35.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
go.mau.fi/util v0.9.8
|
go.mau.fi/util v0.9.9-0.20260430092340-8772e7714ea5
|
||||||
go.mau.fi/webp v0.2.0
|
go.mau.fi/webp v0.2.0
|
||||||
go.mau.fi/zerozap v0.1.2
|
go.mau.fi/zerozap v0.1.2
|
||||||
go.opentelemetry.io/otel v1.42.0
|
go.opentelemetry.io/otel v1.42.0
|
||||||
@@ -42,7 +42,7 @@ require (
|
|||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/tools v0.44.0
|
golang.org/x/tools v0.44.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
maunium.net/go/mautrix v0.27.0
|
maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014
|
||||||
rsc.io/qr v0.2.0
|
rsc.io/qr v0.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
go.mau.fi/util v0.9.8 h1:+/jf8eM2dAT2wx9UidmaneH28r/CSCKCniCyby1qWz8=
|
go.mau.fi/util v0.9.9-0.20260430092340-8772e7714ea5 h1:cNm4gkt7j907g1Q4XvyNKW8tTM8BaU91Kbfa5GGyiCs=
|
||||||
go.mau.fi/util v0.9.8/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
|
go.mau.fi/util v0.9.9-0.20260430092340-8772e7714ea5/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
|
||||||
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
|
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
|
||||||
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
|
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
|
||||||
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
||||||
@@ -236,7 +236,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/mautrix v0.27.0 h1:yfEYwoIluVWkofUgbZl9gP4i5nQTF+QNsxtb+r5bKlM=
|
maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014 h1:KwXGBWwUHYJKVTYWgbZEFcaM6uYLMvfjzHJg/TLwvKc=
|
||||||
maunium.net/go/mautrix v0.27.0/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs=
|
maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk=
|
||||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func (tc *TelegramConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
|
|||||||
return &bridgev2.NetworkGeneralCapabilities{
|
return &bridgev2.NetworkGeneralCapabilities{
|
||||||
DisappearingMessages: true,
|
DisappearingMessages: true,
|
||||||
Provisioning: bridgev2.ProvisioningCapabilities{
|
Provisioning: bridgev2.ProvisioningCapabilities{
|
||||||
|
ImagePackImport: true,
|
||||||
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
|
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
|
||||||
CreateDM: true,
|
CreateDM: true,
|
||||||
LookupPhone: true,
|
LookupPhone: true,
|
||||||
@@ -145,9 +146,9 @@ var fileCaps = event.FileFeatureMap{
|
|||||||
// These are converted to webp
|
// These are converted to webp
|
||||||
"image/jpeg": event.CapLevelPartialSupport,
|
"image/jpeg": event.CapLevelPartialSupport,
|
||||||
"image/png": event.CapLevelPartialSupport,
|
"image/png": event.CapLevelPartialSupport,
|
||||||
// TODO
|
// These will only go through if they're from an imported Telegram pack
|
||||||
//"video/lottie+json": event.CapLevelFullySupported,
|
"video/lottie+json": event.CapLevelPartialSupport,
|
||||||
//"video/webm": event.CapLevelFullySupported,
|
"video/webm": event.CapLevelPartialSupport,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
event.CapMsgVoice: {
|
event.CapMsgVoice: {
|
||||||
|
|||||||
+15
-29
@@ -40,13 +40,13 @@ import (
|
|||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
"maunium.net/go/mautrix/bridgev2/simplevent"
|
"maunium.net/go/mautrix/bridgev2/simplevent"
|
||||||
"maunium.net/go/mautrix/bridgev2/status"
|
"maunium.net/go/mautrix/bridgev2/status"
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/humanise"
|
"go.mau.fi/mautrix-telegram/pkg/connector/humanise"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
|
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/matrixfmt"
|
"go.mau.fi/mautrix-telegram/pkg/connector/matrixfmt"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/store"
|
"go.mau.fi/mautrix-telegram/pkg/connector/store"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/telegramfmt"
|
"go.mau.fi/mautrix-telegram/pkg/connector/telegramfmt"
|
||||||
|
"go.mau.fi/mautrix-telegram/pkg/gotd/pool"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth"
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/updates"
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/updates"
|
||||||
@@ -117,6 +117,10 @@ type TelegramClient struct {
|
|||||||
|
|
||||||
prevReactionPoll map[networkid.PortalKey]time.Time
|
prevReactionPoll map[networkid.PortalKey]time.Time
|
||||||
prevReactionPollLock sync.Mutex
|
prevReactionPollLock sync.Mutex
|
||||||
|
|
||||||
|
stickerPacksByName map[string]*stickerPackCache
|
||||||
|
stickerPacksByID map[int64]*stickerPackCache
|
||||||
|
stickerPackCacheLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ bridgev2.NetworkAPI = (*TelegramClient)(nil)
|
var _ bridgev2.NetworkAPI = (*TelegramClient)(nil)
|
||||||
@@ -171,7 +175,9 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge
|
|||||||
|
|
||||||
takeoutAccepted: exsync.NewEvent(),
|
takeoutAccepted: exsync.NewEvent(),
|
||||||
|
|
||||||
prevReactionPoll: map[networkid.PortalKey]time.Time{},
|
prevReactionPoll: map[networkid.PortalKey]time.Time{},
|
||||||
|
stickerPacksByName: map[string]*stickerPackCache{},
|
||||||
|
stickerPacksByID: map[int64]*stickerPackCache{},
|
||||||
|
|
||||||
recentMessageRooms: exsync.NewRingBuffer[networkid.MessageID, networkid.PortalKey](32),
|
recentMessageRooms: exsync.NewRingBuffer[networkid.MessageID, networkid.PortalKey](32),
|
||||||
|
|
||||||
@@ -341,29 +347,9 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
client.matrixParser = &matrixfmt.HTMLParser{
|
client.matrixParser = &matrixfmt.HTMLParser{
|
||||||
Store: tc.Store,
|
Store: tc.Store,
|
||||||
GetGhostDetails: func(ctx context.Context, portal *bridgev2.Portal, ui id.UserID) (networkid.UserID, string, int64, bool) {
|
Bridge: tc.Bridge,
|
||||||
userID, ok := tc.Bridge.Matrix.ParseGhostMXID(ui)
|
ScopedStore: client.ScopedStore,
|
||||||
if !ok {
|
|
||||||
user, err := tc.Bridge.GetExistingUserByMXID(ctx, ui)
|
|
||||||
if err != nil || user == nil {
|
|
||||||
return "", "", 0, false
|
|
||||||
} else if login, _, _ := portal.FindPreferredLogin(ctx, user, false); login != nil {
|
|
||||||
userID = ids.UserLoginIDToUserID(login.ID)
|
|
||||||
} else {
|
|
||||||
return "", "", 0, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if peerType, telegramUserID, err := ids.ParseUserID(userID); err != nil {
|
|
||||||
return "", "", 0, false
|
|
||||||
} else if accessHash, err := client.ScopedStore.GetAccessHash(ctx, peerType, telegramUserID); err != nil || accessHash == 0 {
|
|
||||||
return "", "", 0, false
|
|
||||||
} else if username, err := client.main.Store.Username.Get(ctx, peerType, telegramUserID); err != nil {
|
|
||||||
return "", "", 0, false
|
|
||||||
} else {
|
|
||||||
return userID, username, accessHash, true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &client, err
|
return &client, err
|
||||||
@@ -418,12 +404,12 @@ func (tc *TelegramClient) onPing() {
|
|||||||
me, err := tc.client.Self(ctx)
|
me, err := tc.client.Self(ctx)
|
||||||
if auth.IsUnauthorized(err) {
|
if auth.IsUnauthorized(err) {
|
||||||
tc.onAuthError(err)
|
tc.onAuthError(err)
|
||||||
} else if errors.Is(err, syscall.EPIPE) {
|
} else if errors.Is(err, syscall.EPIPE) || errors.Is(err, pool.ErrConnDead) {
|
||||||
// This is a pipe error, try disconnecting which will force the
|
// Connectivity error — connection died during the Self() call.
|
||||||
// updatesManager to fail and cause the client to reconnect.
|
// Keep as transient; gotd's backoff will reconnect.
|
||||||
tc.userLogin.BridgeState.Send(status.BridgeState{
|
tc.userLogin.BridgeState.Send(status.BridgeState{
|
||||||
StateEvent: status.StateTransientDisconnect,
|
StateEvent: status.StateTransientDisconnect,
|
||||||
Error: "pipe-error",
|
Error: "connectivity-error",
|
||||||
Message: humanise.Error(err),
|
Message: humanise.Error(err),
|
||||||
})
|
})
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ var cmdEmojiPack = &commands.FullHandler{
|
|||||||
Name: "emoji-pack",
|
Name: "emoji-pack",
|
||||||
Aliases: []string{"pack", "sticker-pack", "emojipack", "stickerpack"},
|
Aliases: []string{"pack", "sticker-pack", "emojipack", "stickerpack"},
|
||||||
Help: commands.HelpMeta{
|
Help: commands.HelpMeta{
|
||||||
Section: commands.HelpSectionChats,
|
Section: commands.HelpSectionMisc,
|
||||||
Description: "Bridge emoji packs between Matrix and Telegram.",
|
Description: "Bridge emoji packs between Matrix and Telegram.",
|
||||||
Args: "<upload/download/list/help> [args...]",
|
Args: "<upload/download/list/help> [args...]",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ proxy:
|
|||||||
# Proxy IP address/domain name and port.
|
# Proxy IP address/domain name and port.
|
||||||
address: "127.0.0.1:1080"
|
address: "127.0.0.1:1080"
|
||||||
# Proxy authentication (optional). Put MTProxy secret in password field.
|
# Proxy authentication (optional). Put MTProxy secret in password field.
|
||||||
|
# For mtproxy, the secret must be hex-encoded (the same form mtg/MTProxy
|
||||||
|
# tools print, e.g. "ee" + 16-byte secret + cloak domain hex for faketls).
|
||||||
username:
|
username:
|
||||||
password:
|
password:
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ import (
|
|||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-telegram/pkg/connector/media"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/message"
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/message"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/uploader"
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/uploader"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
||||||
|
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/emojis"
|
"go.mau.fi/mautrix-telegram/pkg/connector/emojis"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/humanise"
|
"go.mau.fi/mautrix-telegram/pkg/connector/humanise"
|
||||||
@@ -242,10 +244,17 @@ func (tc *TelegramClient) pollSponsoredMessage(ctx context.Context, portal *brid
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TelegramClient) transferMediaToTelegram(ctx context.Context, content *event.MessageEventContent, sticker, forceDocument bool) (tg.InputMediaClass, error) {
|
func (tc *TelegramClient) transferMediaToTelegram(ctx context.Context, content *event.MessageEventContent, sticker, forceRetry, forceDocument bool) (tg.InputMediaClass, error) {
|
||||||
var upload tg.InputFileClass
|
var upload tg.InputFileClass
|
||||||
filename := getMediaFilename(content)
|
filename := getMediaFilename(content)
|
||||||
info := content.GetInfo()
|
info := content.GetInfo()
|
||||||
|
if sticker {
|
||||||
|
if origFile, err := tc.findOriginalStickerDocument(ctx, info.BridgedSticker, forceRetry); err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to find original sticker document, falling back to reupload")
|
||||||
|
} else if origFile != nil {
|
||||||
|
return origFile, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
err := tc.main.Bridge.Bot.DownloadMediaToFile(ctx, content.URL, content.File, false, func(f *os.File) (err error) {
|
err := tc.main.Bridge.Bot.DownloadMediaToFile(ctx, content.URL, content.File, false, func(f *os.File) (err error) {
|
||||||
uploadFilename := f.Name()
|
uploadFilename := f.Name()
|
||||||
if sticker && (info.MimeType == "image/png" || info.MimeType == "image/jpeg") {
|
if sticker && (info.MimeType == "image/png" || info.MimeType == "image/jpeg") {
|
||||||
@@ -267,10 +276,17 @@ func (tc *TelegramClient) transferMediaToTelegram(ctx context.Context, content *
|
|||||||
} else if sticker && (info.MimeType != "video/webm" && info.MimeType != "application/x-tgsticker") {
|
} else if sticker && (info.MimeType != "video/webm" && info.MimeType != "application/x-tgsticker") {
|
||||||
uploadFilename, err = ffmpeg.ConvertPath(ctx, uploadFilename, ".webp", []string{}, []string{}, false)
|
uploadFilename, err = ffmpeg.ConvertPath(ctx, uploadFilename, ".webp", []string{}, []string{}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to convert sticker to webm: %+w", err)
|
return fmt.Errorf("failed to convert sticker to webm: %w", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(uploadFilename)
|
defer os.Remove(uploadFilename)
|
||||||
info.MimeType = "image/webp"
|
info.MimeType = "image/webp"
|
||||||
|
} else if sticker && info.MimeType == "video/lottie+json" {
|
||||||
|
uploadFilename, err = media.CompressGZip(f)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to compress lottie sticker: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(uploadFilename)
|
||||||
|
info.MimeType = "application/x-tgsticker"
|
||||||
} else if cfg, _, err := image.DecodeConfig(f); err != nil {
|
} else if cfg, _, err := image.DecodeConfig(f); err != nil {
|
||||||
forceDocument = true
|
forceDocument = true
|
||||||
} else if fileInfo, err := f.Stat(); err != nil {
|
} else if fileInfo, err := f.Stat(); err != nil {
|
||||||
@@ -458,19 +474,26 @@ func (tc *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2
|
|||||||
|
|
||||||
var updates tg.UpdatesClass
|
var updates tg.UpdatesClass
|
||||||
if msg.Event.Type == event.EventSticker {
|
if msg.Event.Type == event.EventSticker {
|
||||||
var media tg.InputMediaClass
|
mediaReq := &tg.MessagesSendMediaRequest{
|
||||||
media, err = tc.transferMediaToTelegram(ctx, msg.Content, true, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
updates, err = tc.client.API().MessagesSendMedia(ctx, &tg.MessagesSendMediaRequest{
|
|
||||||
Peer: peer,
|
Peer: peer,
|
||||||
Message: message,
|
Message: message,
|
||||||
Entities: entities,
|
Entities: entities,
|
||||||
Media: media,
|
|
||||||
ReplyTo: replyTo,
|
ReplyTo: replyTo,
|
||||||
RandomID: randomID,
|
RandomID: randomID,
|
||||||
})
|
}
|
||||||
|
mediaReq.Media, err = tc.transferMediaToTelegram(ctx, msg.Content, true, false, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updates, err = tc.client.API().MessagesSendMedia(ctx, mediaReq)
|
||||||
|
if tgerr.Is(err, tg.ErrFileReferenceExpired) {
|
||||||
|
zerolog.Ctx(ctx).Debug().AnErr("send_error", err).Msg("Trying to refetch sticker pack")
|
||||||
|
mediaReq.Media, err = tc.transferMediaToTelegram(ctx, msg.Content, true, true, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updates, err = tc.client.API().MessagesSendMedia(ctx, mediaReq)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
switch msg.Content.MsgType {
|
switch msg.Content.MsgType {
|
||||||
case event.MsgText, event.MsgNotice, event.MsgEmote:
|
case event.MsgText, event.MsgNotice, event.MsgEmote:
|
||||||
@@ -485,7 +508,7 @@ func (tc *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2
|
|||||||
case event.MsgImage, event.MsgFile, event.MsgAudio, event.MsgVideo:
|
case event.MsgImage, event.MsgFile, event.MsgAudio, event.MsgVideo:
|
||||||
var media tg.InputMediaClass
|
var media tg.InputMediaClass
|
||||||
forceDocument, _ := msg.Event.Content.Raw["fi.mau.telegram.force_document"].(bool)
|
forceDocument, _ := msg.Event.Content.Raw["fi.mau.telegram.force_document"].(bool)
|
||||||
media, err = tc.transferMediaToTelegram(ctx, msg.Content, false, forceDocument)
|
media, err = tc.transferMediaToTelegram(ctx, msg.Content, false, false, forceDocument)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -650,7 +673,7 @@ func (tc *TelegramClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Ma
|
|||||||
} else {
|
} else {
|
||||||
log.Info().Msg("media URI changed, re-uploading media")
|
log.Info().Msg("media URI changed, re-uploading media")
|
||||||
forceDocument, _ := msg.Event.Content.Raw["fi.mau.telegram.force_document"].(bool)
|
forceDocument, _ := msg.Event.Content.Raw["fi.mau.telegram.force_document"].(bool)
|
||||||
req.Media, err = tc.transferMediaToTelegram(ctx, msg.Content, false, forceDocument)
|
req.Media, err = tc.transferMediaToTelegram(ctx, msg.Content, false, false, forceDocument)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -746,7 +769,7 @@ func (tc *TelegramClient) PreHandleMatrixReaction(ctx context.Context, msg *brid
|
|||||||
|
|
||||||
keyNoVariation := variationselector.Remove(msg.Content.RelatesTo.Key)
|
keyNoVariation := variationselector.Remove(msg.Content.RelatesTo.Key)
|
||||||
emojiID := ids.MakeEmojiIDFromEmoticon(msg.Content.RelatesTo.Key)
|
emojiID := ids.MakeEmojiIDFromEmoticon(msg.Content.RelatesTo.Key)
|
||||||
if strings.HasPrefix(msg.Content.RelatesTo.Key, "mxc://") {
|
if strings.Contains(msg.Content.RelatesTo.Key, "://") {
|
||||||
if file, err := tc.main.Store.TelegramFile.GetByMXC(ctx, id.ContentURIString(msg.Content.RelatesTo.Key)); err != nil {
|
if file, err := tc.main.Store.TelegramFile.GetByMXC(ctx, id.ContentURIString(msg.Content.RelatesTo.Key)); err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
} else if file == nil {
|
} else if file == nil {
|
||||||
|
|||||||
@@ -923,6 +923,8 @@ func (tc *TelegramClient) onUpdate(ctx context.Context, e tg.Entities, upd tg.Up
|
|||||||
return tc.onMessageEdit(ctx, update)
|
return tc.onMessageEdit(ctx, update)
|
||||||
case *tg.UpdateMessageReactions:
|
case *tg.UpdateMessageReactions:
|
||||||
return tc.onMessageReactions(ctx, update)
|
return tc.onMessageReactions(ctx, update)
|
||||||
|
case *tg.UpdateBotMessageReaction:
|
||||||
|
return tc.onBotMessageReaction(ctx, update)
|
||||||
case *tg.UpdateUserTyping:
|
case *tg.UpdateUserTyping:
|
||||||
return tc.handleTyping(tc.makePortalKeyFromID(ids.PeerTypeUser, update.UserID, 0), tc.senderForUserID(update.UserID), update.Action)
|
return tc.handleTyping(tc.makePortalKeyFromID(ids.PeerTypeUser, update.UserID, 0), tc.senderForUserID(update.UserID), update.Action)
|
||||||
case *tg.UpdateChatUserTyping:
|
case *tg.UpdateChatUserTyping:
|
||||||
@@ -964,6 +966,78 @@ func (tc *TelegramClient) onMessageReactions(ctx context.Context, update *tg.Upd
|
|||||||
return tc.handleTelegramReactions(ctx, update.Peer, update.TopMsgID, update.MsgID, update.Reactions, "updateMessageReactions")
|
return tc.handleTelegramReactions(ctx, update.Peer, update.TopMsgID, update.MsgID, update.Reactions, "updateMessageReactions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tc *TelegramClient) onBotMessageReaction(ctx context.Context, update *tg.UpdateBotMessageReaction) error {
|
||||||
|
wrappedMessageID := ids.MakeMessageID(update.Peer, update.MsgID)
|
||||||
|
var portalKey networkid.PortalKey
|
||||||
|
var ok bool
|
||||||
|
if portalKey, ok = tc.recentMessageRooms.Get(wrappedMessageID); ok {
|
||||||
|
// key found in cache
|
||||||
|
} else if parts, err := tc.main.Bridge.DB.Message.GetAllPartsByID(ctx, tc.loginID, wrappedMessageID); err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(parts) > 0 {
|
||||||
|
portalKey = parts[0].Room
|
||||||
|
} else {
|
||||||
|
// This won't work for topics, but hopefully the cases above will cover most messages
|
||||||
|
portalKey = tc.makePortalKeyFromPeer(update.Peer, 0)
|
||||||
|
}
|
||||||
|
var eventSender bridgev2.EventSender
|
||||||
|
switch update.Actor.(type) {
|
||||||
|
case *tg.PeerUser, *tg.PeerChannel:
|
||||||
|
eventSender = tc.getPeerSender(update.Actor)
|
||||||
|
default:
|
||||||
|
zerolog.Ctx(ctx).Warn().
|
||||||
|
Type("actor_type", update.Actor).
|
||||||
|
Msg("Unexpected actor type in bot message reaction")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var customEmojiIDs []int64
|
||||||
|
for _, reaction := range update.NewReactions {
|
||||||
|
if e, ok := reaction.(*tg.ReactionCustomEmoji); ok {
|
||||||
|
customEmojiIDs = append(customEmojiIDs, e.DocumentID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customEmojis, err := tc.transferEmojisToMatrix(ctx, customEmojiIDs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to transfer custom emojis for bot message reaction: %w", err)
|
||||||
|
}
|
||||||
|
reactions := make([]*bridgev2.BackfillReaction, 0, len(update.NewReactions))
|
||||||
|
for _, reaction := range update.NewReactions {
|
||||||
|
emojiID, emoji, err := computeEmojiAndID(reaction, customEmojis)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to compute emoji and ID for reaction")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reactions = append(reactions, &bridgev2.BackfillReaction{
|
||||||
|
Timestamp: time.Unix(int64(update.Date), 0),
|
||||||
|
Sender: eventSender,
|
||||||
|
EmojiID: emojiID,
|
||||||
|
Emoji: emoji,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultToError(tc.main.Bridge.QueueRemoteEvent(tc.userLogin, &simplevent.ReactionSync{
|
||||||
|
EventMeta: simplevent.EventMeta{
|
||||||
|
Type: bridgev2.RemoteEventReactionSync,
|
||||||
|
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||||
|
return c.
|
||||||
|
Int("message_id", update.MsgID).
|
||||||
|
Any("peer_id", update.Peer).
|
||||||
|
Str("sync_source", "updateBotMessageReaction")
|
||||||
|
},
|
||||||
|
PortalKey: portalKey,
|
||||||
|
},
|
||||||
|
TargetMessage: wrappedMessageID,
|
||||||
|
Reactions: &bridgev2.ReactionSyncData{
|
||||||
|
Users: map[networkid.UserID]*bridgev2.ReactionSyncUser{
|
||||||
|
eventSender.Sender: {
|
||||||
|
Reactions: reactions,
|
||||||
|
HasAllReactions: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func (tc *TelegramClient) onMessageEdit(ctx context.Context, update IGetMessage) error {
|
func (tc *TelegramClient) onMessageEdit(ctx context.Context, update IGetMessage) error {
|
||||||
msg, ok := update.GetMessage().(*tg.Message)
|
msg, ok := update.GetMessage().(*tg.Message)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
+280
-80
@@ -31,6 +31,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"go.mau.fi/util/emojishortcodes"
|
||||||
"go.mau.fi/util/exmaps"
|
"go.mau.fi/util/exmaps"
|
||||||
"go.mau.fi/util/ffmpeg"
|
"go.mau.fi/util/ffmpeg"
|
||||||
"go.mau.fi/util/variationselector"
|
"go.mau.fi/util/variationselector"
|
||||||
@@ -448,89 +450,20 @@ func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) {
|
|||||||
ce.Reply("Can't bridge image packs if personal filtering spaces are disabled")
|
ce.Reply("Can't bridge image packs if personal filtering spaces are disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var input tg.InputStickerSetClass
|
|
||||||
if match := addStickersRegex.FindStringSubmatch(ce.Args[0]); match != nil {
|
|
||||||
input = &tg.InputStickerSetShortName{ShortName: match[1]}
|
|
||||||
} else if packShortcodeRegex.MatchString(ce.Args[0]) {
|
|
||||||
input = &tg.InputStickerSetShortName{ShortName: ce.Args[0]}
|
|
||||||
} else {
|
|
||||||
ce.Reply("Invalid pack shortcode or link")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rawSet, err := tc.client.API().MessagesGetStickerSet(ce.Ctx, &tg.MessagesGetStickerSetRequest{Stickerset: input})
|
|
||||||
if err != nil {
|
|
||||||
ce.Reply("Failed to get sticker set: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
set, ok := rawSet.(*tg.MessagesStickerSet)
|
|
||||||
if !ok {
|
|
||||||
ce.Reply("Unexpected response type: %T", rawSet)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
linkType := "addstickers"
|
|
||||||
usage := event.ImagePackUsageSticker
|
|
||||||
if set.Set.Emojis {
|
|
||||||
linkType = "addemoji"
|
|
||||||
usage = event.ImagePackUsageEmoji
|
|
||||||
}
|
|
||||||
pack := &event.ImagePackEventContent{
|
|
||||||
Images: make(map[string]*event.ImagePackImage, len(set.Documents)),
|
|
||||||
Metadata: event.ImagePackMetadata{
|
|
||||||
DisplayName: set.Set.Title,
|
|
||||||
AvatarURL: "",
|
|
||||||
Usage: []event.ImagePackUsage{usage},
|
|
||||||
Attribution: fmt.Sprintf("Imported from https://t.me/%s/%s", linkType, set.Set.ShortName),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
keywords := make(map[int64][]string)
|
|
||||||
emojis := make(map[int64][]string)
|
|
||||||
for _, kw := range set.Keywords {
|
|
||||||
keywords[kw.DocumentID] = kw.Keyword
|
|
||||||
}
|
|
||||||
for _, emojiPack := range set.Packs {
|
|
||||||
emoji := variationselector.Add(emojiPack.Emoticon)
|
|
||||||
for _, doc := range emojiPack.Documents {
|
|
||||||
emojis[doc] = append(emojis[doc], emoji)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
evtID := ce.React("\u23f3\ufe0f")
|
evtID := ce.React("\u23f3\ufe0f")
|
||||||
defer redactReaction(ce, evtID)
|
defer redactReaction(ce, evtID)
|
||||||
for i, rawDoc := range set.Documents {
|
pack, err := tc.DownloadImagePack(ce.Ctx, ce.Args[0])
|
||||||
mxc, _, info, err := media.NewTransferer(tc.client.API()).
|
if err != nil {
|
||||||
WithStickerConfig(tc.main.Config.AnimatedSticker).
|
ce.Reply("Failed to import pack: %v", err)
|
||||||
WithForceWebmStickerConvert(set.Set.Emojis).
|
return
|
||||||
WithDocument(rawDoc, false).
|
|
||||||
Transfer(ce.Ctx, tc.main.Store, tc.main.Bridge.Bot)
|
|
||||||
if err != nil {
|
|
||||||
ce.Log.Err(err).Msg("Failed to transfer image in pack")
|
|
||||||
ce.Reply("Failed to transfer document `%d`: %v", rawDoc.GetID(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
kws := keywords[rawDoc.GetID()]
|
|
||||||
imageEmojis := emojis[rawDoc.GetID()]
|
|
||||||
var key string
|
|
||||||
for _, kw := range kws {
|
|
||||||
_, alreadySet := pack.Images[kw]
|
|
||||||
if alreadySet {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key = kw
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if key == "" {
|
|
||||||
key = fmt.Sprintf("%s_img%d", set.Set.ShortName, i+1)
|
|
||||||
}
|
|
||||||
body := key
|
|
||||||
if len(imageEmojis) > 0 {
|
|
||||||
body = imageEmojis[0]
|
|
||||||
}
|
|
||||||
pack.Images[key] = &event.ImagePackImage{
|
|
||||||
URL: mxc,
|
|
||||||
Body: body,
|
|
||||||
Info: info,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_, err = tc.main.Bridge.Bot.SendState(ce.Ctx, spaceRoom, event.StateUnstableImagePack, set.Set.ShortName, &event.Content{Parsed: pack}, time.Now())
|
if pack.Shortcode == "" && pack.Content.Metadata.BridgedPack != nil {
|
||||||
|
pack.Shortcode = pack.Content.Metadata.BridgedPack.URL
|
||||||
|
}
|
||||||
|
_, err = tc.main.Bridge.Bot.SendState(ce.Ctx, spaceRoom, event.StateUnstableImagePack, pack.Shortcode, &event.Content{
|
||||||
|
Parsed: pack.Content,
|
||||||
|
Raw: pack.Extra,
|
||||||
|
}, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ce.Reply("Failed to send image pack to space: %v", err)
|
ce.Reply("Failed to send image pack to space: %v", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -540,3 +473,270 @@ func (tc *TelegramClient) fnDownloadEmojiPack(ce *commands.Event) {
|
|||||||
spaceRoom.URI(tc.main.Bridge.Matrix.ServerName()).MatrixToURL()))
|
spaceRoom.URI(tc.main.Bridge.Matrix.ServerName()).MatrixToURL()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tc *TelegramClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
|
||||||
|
resp, err := tc.client.API().MessagesGetAllStickers(ctx, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
casted, ok := resp.(*tg.MessagesAllStickers)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected response type: %T", resp)
|
||||||
|
}
|
||||||
|
packs := make([]*event.ImagePackMetadata, len(casted.Sets))
|
||||||
|
for i, set := range casted.Sets {
|
||||||
|
packs[i] = tc.makeImagePackMetadata(ctx, set)
|
||||||
|
}
|
||||||
|
return packs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TelegramClient) makeImagePackMetadata(ctx context.Context, pack tg.StickerSet) *event.ImagePackMetadata {
|
||||||
|
linkType := "addstickers"
|
||||||
|
usage := event.ImagePackUsageSticker
|
||||||
|
if pack.Emojis {
|
||||||
|
linkType = "addemoji"
|
||||||
|
usage = event.ImagePackUsageEmoji
|
||||||
|
}
|
||||||
|
packURL := fmt.Sprintf("https://t.me/%s/%s", linkType, pack.ShortName)
|
||||||
|
return &event.ImagePackMetadata{
|
||||||
|
DisplayName: pack.Title,
|
||||||
|
AvatarURL: "", // TODO
|
||||||
|
Usage: []event.ImagePackUsage{usage},
|
||||||
|
Attribution: fmt.Sprintf("Imported from %s", packURL),
|
||||||
|
BridgedPack: &event.BridgedStickerPack{
|
||||||
|
Network: StickerSourceID,
|
||||||
|
URL: packURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TelegramClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
|
||||||
|
var shortName string
|
||||||
|
if match := addStickersRegex.FindStringSubmatch(url); match != nil {
|
||||||
|
shortName = match[1]
|
||||||
|
} else if packShortcodeRegex.MatchString(url) {
|
||||||
|
shortName = url
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid pack shortcode or link: %s", url)
|
||||||
|
}
|
||||||
|
rawSet, err := tc.client.API().MessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{Stickerset: &tg.InputStickerSetShortName{ShortName: shortName}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
set, ok := rawSet.(*tg.MessagesStickerSet)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected response type: %T", rawSet)
|
||||||
|
}
|
||||||
|
tc.addStickerPackToCache(set, true)
|
||||||
|
pack := &event.ImagePackEventContent{
|
||||||
|
Images: make(map[string]*event.ImagePackImage, len(set.Documents)),
|
||||||
|
Metadata: *tc.makeImagePackMetadata(ctx, set.Set),
|
||||||
|
}
|
||||||
|
topLevelExtra := map[string]any{
|
||||||
|
"fi.mau.telegram.stickerpack": map[string]any{
|
||||||
|
"id": strconv.FormatInt(set.Set.ID, 10),
|
||||||
|
"short_name": set.Set.ShortName,
|
||||||
|
"emoji_pack": set.Set.Emojis,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
keywords := make(map[int64][]string)
|
||||||
|
emojiLists := make(map[int64][]string)
|
||||||
|
for _, kw := range set.Keywords {
|
||||||
|
keywords[kw.DocumentID] = kw.Keyword
|
||||||
|
}
|
||||||
|
for _, emojiPack := range set.Packs {
|
||||||
|
emoji := variationselector.Add(emojiPack.Emoticon)
|
||||||
|
for _, doc := range emojiPack.Documents {
|
||||||
|
emojiLists[doc] = append(emojiLists[doc], emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, rawDoc := range set.Documents {
|
||||||
|
// TODO use direct media
|
||||||
|
mxc, _, info, err := media.NewTransferer(tc.client.API()).
|
||||||
|
WithStickerConfig(tc.main.Config.AnimatedSticker).
|
||||||
|
WithForceWebmStickerConvert(set.Set.Emojis).
|
||||||
|
WithDocument(rawDoc, false).
|
||||||
|
Transfer(ctx, tc.main.Store, tc.main.Bridge.Bot)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to transfer image in pack")
|
||||||
|
return nil, fmt.Errorf("failed to transfer document %d: %w", rawDoc.GetID(), err)
|
||||||
|
}
|
||||||
|
kws := keywords[rawDoc.GetID()]
|
||||||
|
imageEmojis := emojiLists[rawDoc.GetID()]
|
||||||
|
var key string
|
||||||
|
for _, kw := range kws {
|
||||||
|
_, alreadySet := pack.Images[kw]
|
||||||
|
if alreadySet {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key = kw
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var firstShortcode string
|
||||||
|
if key == "" {
|
||||||
|
for _, emoji := range imageEmojis {
|
||||||
|
shortcode := emojishortcodes.Get(emoji)
|
||||||
|
if shortcode == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
shortcode = fmt.Sprintf("%s_%s", set.Set.ShortName, shortcode)
|
||||||
|
if firstShortcode == "" {
|
||||||
|
firstShortcode = shortcode
|
||||||
|
}
|
||||||
|
_, alreadySet := pack.Images[shortcode]
|
||||||
|
if alreadySet {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key = shortcode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if key == "" && firstShortcode != "" {
|
||||||
|
for i := 2; i < 10000; i++ {
|
||||||
|
kw := fmt.Sprintf("%s%d", firstShortcode, i)
|
||||||
|
_, alreadySet := pack.Images[kw]
|
||||||
|
if alreadySet {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key = kw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
key = fmt.Sprintf("%s_img%d", set.Set.ShortName, i+1)
|
||||||
|
}
|
||||||
|
var emoji string
|
||||||
|
if len(imageEmojis) > 0 {
|
||||||
|
emoji = imageEmojis[0]
|
||||||
|
}
|
||||||
|
if !set.Set.Emojis {
|
||||||
|
// Stickers need extra info in each sticker so they can be accurately bridged back to Telegram
|
||||||
|
// Custom emojis don't have space for such info and can be used with just the document ID
|
||||||
|
info.BridgedSticker = &event.BridgedSticker{
|
||||||
|
Network: StickerSourceID,
|
||||||
|
ID: strconv.FormatInt(rawDoc.GetID(), 10),
|
||||||
|
PackURL: StickerPackURLPrefix + set.Set.ShortName,
|
||||||
|
Emoji: emoji,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pack.Images[key] = &event.ImagePackImage{
|
||||||
|
URL: mxc,
|
||||||
|
Body: cmp.Or(emoji, key),
|
||||||
|
Info: info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &bridgev2.ImportedImagePack{
|
||||||
|
Content: pack,
|
||||||
|
Extra: topLevelExtra,
|
||||||
|
Shortcode: set.Set.ShortName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const StickerSourceID = "telegram"
|
||||||
|
const StickerPackURLPrefix = "https://t.me/addstickers/"
|
||||||
|
|
||||||
|
func (tc *TelegramClient) stickerSourceFromAttribute(ctx context.Context, documentID int64, attr *tg.DocumentAttributeSticker) *event.BridgedSticker {
|
||||||
|
var shortName string
|
||||||
|
switch set := attr.Stickerset.(type) {
|
||||||
|
case *tg.InputStickerSetID:
|
||||||
|
pack, err := tc.GetCachedStickerPack(ctx, "", set, false)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Debug().Err(err).
|
||||||
|
Int64("pack_id", set.ID).
|
||||||
|
Msg("Failed to get sticker pack by ID to fill info")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
shortName = pack.meta.ShortName
|
||||||
|
case *tg.InputStickerSetShortName:
|
||||||
|
shortName = set.ShortName
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &event.BridgedSticker{
|
||||||
|
Network: StickerSourceID,
|
||||||
|
ID: strconv.FormatInt(documentID, 10),
|
||||||
|
Emoji: attr.Alt,
|
||||||
|
PackURL: StickerPackURLPrefix + shortName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type stickerPackCache struct {
|
||||||
|
docs map[int64]*tg.Document
|
||||||
|
meta tg.StickerSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TelegramClient) GetCachedStickerPack(ctx context.Context, shortName string, id *tg.InputStickerSetID, forceClearCache bool) (*stickerPackCache, error) {
|
||||||
|
tc.stickerPackCacheLock.Lock()
|
||||||
|
defer tc.stickerPackCacheLock.Unlock()
|
||||||
|
cacheName := strings.ToLower(shortName)
|
||||||
|
cache, ok := tc.stickerPacksByName[cacheName]
|
||||||
|
if !ok {
|
||||||
|
cache, ok = tc.stickerPacksByID[id.GetID()]
|
||||||
|
}
|
||||||
|
if !ok || forceClearCache {
|
||||||
|
var inputSet tg.InputStickerSetClass = id
|
||||||
|
if id == nil {
|
||||||
|
inputSet = &tg.InputStickerSetShortName{ShortName: shortName}
|
||||||
|
}
|
||||||
|
resp, err := tc.client.API().MessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{Stickerset: inputSet})
|
||||||
|
if err != nil {
|
||||||
|
if tgerr.Is(err, tg.ErrStickersetInvalid) {
|
||||||
|
if cacheName != "" {
|
||||||
|
tc.stickerPacksByName[cacheName] = nil
|
||||||
|
}
|
||||||
|
if id != nil {
|
||||||
|
tc.stickerPacksByID[id.GetID()] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to get sticker set: %w", err)
|
||||||
|
}
|
||||||
|
set, ok := resp.AsModified()
|
||||||
|
if !ok {
|
||||||
|
if cacheName != "" {
|
||||||
|
tc.stickerPacksByName[cacheName] = nil
|
||||||
|
}
|
||||||
|
if id != nil {
|
||||||
|
tc.stickerPacksByID[id.GetID()] = nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected response type for MessagesGetStickerSet: %T", resp)
|
||||||
|
}
|
||||||
|
cache = tc.addStickerPackToCache(set, false)
|
||||||
|
}
|
||||||
|
return cache, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TelegramClient) addStickerPackToCache(set *tg.MessagesStickerSet, lock bool) *stickerPackCache {
|
||||||
|
if lock {
|
||||||
|
tc.stickerPackCacheLock.Lock()
|
||||||
|
defer tc.stickerPackCacheLock.Unlock()
|
||||||
|
}
|
||||||
|
cache := &stickerPackCache{
|
||||||
|
docs: set.MapDocuments().DocumentToMap(),
|
||||||
|
meta: set.Set,
|
||||||
|
}
|
||||||
|
tc.stickerPacksByName[strings.ToLower(set.Set.ShortName)] = cache
|
||||||
|
tc.stickerPacksByID[set.Set.ID] = cache
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TelegramClient) findOriginalStickerDocument(ctx context.Context, meta *event.BridgedSticker, forceClearCache bool) (tg.InputMediaClass, error) {
|
||||||
|
if meta == nil || !strings.HasPrefix(meta.PackURL, StickerPackURLPrefix) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
shortName := strings.TrimPrefix(meta.PackURL, StickerPackURLPrefix)
|
||||||
|
if shortName == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
idNum, err := strconv.ParseInt(meta.ID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
cache, err := tc.GetCachedStickerPack(ctx, shortName, nil, forceClearCache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stickerDoc, ok := cache.docs[idNum]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &tg.InputMediaDocument{ID: stickerDoc.AsInput()}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
@@ -31,6 +32,7 @@ import (
|
|||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/emojis"
|
"go.mau.fi/mautrix-telegram/pkg/connector/emojis"
|
||||||
|
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/store"
|
"go.mau.fi/mautrix-telegram/pkg/connector/store"
|
||||||
"go.mau.fi/mautrix-telegram/pkg/connector/telegramfmt"
|
"go.mau.fi/mautrix-telegram/pkg/connector/telegramfmt"
|
||||||
)
|
)
|
||||||
@@ -108,6 +110,10 @@ func (es *EntityString) TrimSpace() *EntityString {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if cutStart == len(es.String) {
|
||||||
|
DebugLog(" -> ALLSPACE\n")
|
||||||
|
return &EntityString{}
|
||||||
|
}
|
||||||
for cutEnd = len(es.String) - 1; cutEnd >= 0; cutEnd-- {
|
for cutEnd = len(es.String) - 1; cutEnd >= 0; cutEnd-- {
|
||||||
switch es.String[cutEnd] {
|
switch es.String[cutEnd] {
|
||||||
case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0:
|
case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0:
|
||||||
@@ -254,8 +260,9 @@ func (ctx Context) WithIncrementedListDepth() Context {
|
|||||||
|
|
||||||
// HTMLParser is a somewhat customizable Matrix HTML parser.
|
// HTMLParser is a somewhat customizable Matrix HTML parser.
|
||||||
type HTMLParser struct {
|
type HTMLParser struct {
|
||||||
GetGhostDetails func(context.Context, *bridgev2.Portal, id.UserID) (networkid.UserID, string, int64, bool)
|
Bridge *bridgev2.Bridge
|
||||||
Store *store.Container
|
Store *store.Container
|
||||||
|
ScopedStore *store.ScopedStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaggedString is a string that also contains a HTML tag.
|
// TaggedString is a string that also contains a HTML tag.
|
||||||
@@ -369,13 +376,38 @@ func (parser *HTMLParser) headerToString(node *html.Node, ctx Context) *EntitySt
|
|||||||
return NewEntityString(prefix).Append(parser.nodeToString(node.FirstChild, ctx)).Format(telegramfmt.Style{Type: telegramfmt.StyleBold})
|
return NewEntityString(prefix).Append(parser.nodeToString(node.FirstChild, ctx)).Format(telegramfmt.Style{Type: telegramfmt.StyleBold})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (parser *HTMLParser) getGhostDetails(ctx context.Context, portal *bridgev2.Portal, ui id.UserID) (networkid.UserID, string, int64, bool) {
|
||||||
|
userID, ok := parser.Bridge.Matrix.ParseGhostMXID(ui)
|
||||||
|
if !ok {
|
||||||
|
user, err := parser.Bridge.GetExistingUserByMXID(ctx, ui)
|
||||||
|
if err != nil || user == nil {
|
||||||
|
return "", "", 0, false
|
||||||
|
} else if login, _, _ := portal.FindPreferredLogin(ctx, user, false); login != nil {
|
||||||
|
userID = ids.UserLoginIDToUserID(login.ID)
|
||||||
|
} else {
|
||||||
|
return "", "", 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if peerType, telegramUserID, err := ids.ParseUserID(userID); err != nil {
|
||||||
|
return "", "", 0, false
|
||||||
|
} else if accessHash, err := parser.ScopedStore.GetAccessHash(ctx, peerType, telegramUserID); err != nil || accessHash == 0 {
|
||||||
|
return "", "", 0, false
|
||||||
|
} else if username, err := parser.Store.Username.Get(ctx, peerType, telegramUserID); err != nil {
|
||||||
|
return "", "", 0, false
|
||||||
|
} else {
|
||||||
|
return userID, username, accessHash, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (parser *HTMLParser) linkToString(node *html.Node, ctx Context) *EntityString {
|
func (parser *HTMLParser) linkToString(node *html.Node, ctx Context) *EntityString {
|
||||||
str := parser.nodeToTagAwareString(node.FirstChild, ctx)
|
str := parser.nodeToTagAwareString(node.FirstChild, ctx)
|
||||||
href := parser.getAttribute(node, "href")
|
href := parser.getAttribute(node, "href")
|
||||||
if len(href) == 0 {
|
if len(href) == 0 {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
ent := NewEntityString(str.String.String())
|
linkText := str.String.String()
|
||||||
|
linkTextEnt := NewEntityString(linkText)
|
||||||
|
isRawLink := linkText == href
|
||||||
|
|
||||||
parsedMatrix, err := id.ParseMatrixURIOrMatrixToURL(href)
|
parsedMatrix, err := id.ParseMatrixURIOrMatrixToURL(href)
|
||||||
if err == nil && parsedMatrix != nil && parsedMatrix.Sigil1 == '@' {
|
if err == nil && parsedMatrix != nil && parsedMatrix.Sigil1 == '@' {
|
||||||
@@ -384,19 +416,37 @@ func (parser *HTMLParser) linkToString(node *html.Node, ctx Context) *EntityStri
|
|||||||
// Mention not allowed, use name as-is
|
// Mention not allowed, use name as-is
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
userID, username, accessHash, ok := parser.GetGhostDetails(ctx.Ctx, ctx.Portal, mxid)
|
userID, username, accessHash, ok := parser.getGhostDetails(ctx.Ctx, ctx.Portal, mxid)
|
||||||
if !ok {
|
if !ok {
|
||||||
return str
|
return str
|
||||||
} else if username == "" {
|
} else if username == "" {
|
||||||
return ent.Format(telegramfmt.Mention{UserID: userID, AccessHash: accessHash})
|
return linkTextEnt.Format(telegramfmt.Mention{UserID: userID, AccessHash: accessHash})
|
||||||
} else {
|
} else {
|
||||||
return NewEntityString("@" + username).Format(telegramfmt.Mention{UserID: userID, Username: username})
|
return NewEntityString("@" + username).Format(telegramfmt.Mention{UserID: userID, Username: username})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if str.String.String() == href {
|
if parsedMatrix != nil && parsedMatrix.Sigil1 == '!' && parsedMatrix.Sigil2 == '$' {
|
||||||
return ent.Format(telegramfmt.Style{Type: telegramfmt.StyleURL, URL: href})
|
msg, err := parser.Bridge.DB.Message.GetPartByMXID(ctx.Ctx, parsedMatrix.EventID())
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx.Ctx).Err(err).Msg("Failed to get message for event ID in link")
|
||||||
|
} else if msg != nil {
|
||||||
|
_, chatID, topicID, _ := ids.ParsePortalID(msg.Room.ID)
|
||||||
|
_, msgID, _ := ids.ParseMessageID(msg.ID)
|
||||||
|
if msgID != 0 && chatID != 0 {
|
||||||
|
href = fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID)
|
||||||
|
if topicID > 0 {
|
||||||
|
href = fmt.Sprintf("https://t.me/c/%d/%d/%d", chatID, topicID, msgID)
|
||||||
|
}
|
||||||
|
if isRawLink {
|
||||||
|
linkTextEnt = NewEntityString(href)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isRawLink {
|
||||||
|
return linkTextEnt.Format(telegramfmt.Style{Type: telegramfmt.StyleURL, URL: href})
|
||||||
} else {
|
} else {
|
||||||
return ent.Format(telegramfmt.Style{Type: telegramfmt.StyleTextURL, URL: href})
|
return linkTextEnt.Format(telegramfmt.Style{Type: telegramfmt.StyleTextURL, URL: href})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,30 @@ func (c *AnimatedStickerConfig) convertWebm(ctx context.Context, src *os.File) *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CompressGZip(src *os.File) (replPath string, err error) {
|
||||||
|
tempFile, err := os.CreateTemp("", "telegram-sticker-gzip-*.tgs")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
writer := gzip.NewWriter(tempFile)
|
||||||
|
defer func() {
|
||||||
|
_ = tempFile.Close()
|
||||||
|
_ = writer.Close()
|
||||||
|
if replPath == "" {
|
||||||
|
_ = os.Remove(tempFile.Name())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err = io.Copy(writer, src)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to compress lottie gzip: %w", err)
|
||||||
|
}
|
||||||
|
err = writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to close gzip writer: %w", err)
|
||||||
|
}
|
||||||
|
return tempFile.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func extractGZip(src *os.File) (*ConvertedSticker, error) {
|
func extractGZip(src *os.File) (*ConvertedSticker, error) {
|
||||||
reader, err := gzip.NewReader(src)
|
reader, err := gzip.NewReader(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -144,6 +144,11 @@ func (t *Transferer) WithStickerConfig(cfg AnimatedStickerConfig) *Transferer {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Transferer) WithStickerMetadata(meta *event.BridgedSticker) *Transferer {
|
||||||
|
t.fileInfo.BridgedSticker = meta
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Transferer) WithForceWebmStickerConvert(force bool) *Transferer {
|
func (t *Transferer) WithForceWebmStickerConvert(force bool) *Transferer {
|
||||||
if force {
|
if force {
|
||||||
t.animatedStickerConfig.ConvertFromWebm = true
|
t.animatedStickerConfig.ConvertFromWebm = true
|
||||||
@@ -197,6 +202,11 @@ func (t *Transferer) WithVideo(attr *tg.DocumentAttributeVideo) *Transferer {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Transferer) WithAudio(attr *tg.DocumentAttributeAudio) *Transferer {
|
||||||
|
t.fileInfo.Duration = attr.Duration * 1000
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Transferer) WithImageSize(attr *tg.DocumentAttributeImageSize) *Transferer {
|
func (t *Transferer) WithImageSize(attr *tg.DocumentAttributeImageSize) *Transferer {
|
||||||
t.fileInfo.Width, t.fileInfo.Height = attr.W, attr.H
|
t.fileInfo.Width, t.fileInfo.Height = attr.W, attr.H
|
||||||
t.adjustStickerSize()
|
t.adjustStickerSize()
|
||||||
|
|||||||
+23
-1
@@ -17,13 +17,31 @@
|
|||||||
package connector
|
package connector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/dcs"
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/dcs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// decodeMTProxySecret parses an MTProxy secret string into raw bytes.
|
||||||
|
// MTProxy secrets are binary (faketls secrets begin with 0xEE, secured with 0xDD)
|
||||||
|
// and cannot be carried verbatim in a YAML string field, so we accept the standard
|
||||||
|
// hex encoding (optionally prefixed with "ee"/"dd") used by mtg/MTProxy tooling.
|
||||||
|
func decodeMTProxySecret(s string) ([]byte, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return nil, fmt.Errorf("mtproxy secret is empty")
|
||||||
|
}
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mtproxy secret must be hex-encoded: %w", err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetProxyDialFunc(cfg ProxyConfig) (dcs.DialFunc, error) {
|
func GetProxyDialFunc(cfg ProxyConfig) (dcs.DialFunc, error) {
|
||||||
switch cfg.Type {
|
switch cfg.Type {
|
||||||
// we can't proxy HTTP through mtproxy
|
// we can't proxy HTTP through mtproxy
|
||||||
@@ -54,7 +72,11 @@ func GetProxyResolver(cfg ProxyConfig) (dcs.Resolver, error) {
|
|||||||
resolver := dcs.Plain(dcs.PlainOptions{Dial: dialer})
|
resolver := dcs.Plain(dcs.PlainOptions{Dial: dialer})
|
||||||
return resolver, nil
|
return resolver, nil
|
||||||
case "mtproxy":
|
case "mtproxy":
|
||||||
return dcs.MTProxy(cfg.Address, []byte(cfg.Password), dcs.MTProxyOptions{})
|
secret, err := decodeMTProxySecret(cfg.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dcs.MTProxy(cfg.Address, secret, dcs.MTProxyOptions{})
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported proxy type %s", cfg.Type)
|
return nil, fmt.Errorf("unsupported proxy type %s", cfg.Type)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package connector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeMTProxySecret(t *testing.T) {
|
||||||
|
// faketls secret: 0xee + 16 bytes + cloak domain ("working-name.ru" = 15 bytes)
|
||||||
|
hexSecret := "ee971746d927f4c0138b18447bfe1269bc70312e776f726b696e672d6e616d652e7275"
|
||||||
|
want := []byte{
|
||||||
|
0xee,
|
||||||
|
0x97, 0x17, 0x46, 0xd9, 0x27, 0xf4, 0xc0, 0x13,
|
||||||
|
0x8b, 0x18, 0x44, 0x7b, 0xfe, 0x12, 0x69, 0xbc,
|
||||||
|
0x70, 0x31, 0x2e, 0x77, 0x6f, 0x72, 0x6b, 0x69,
|
||||||
|
0x6e, 0x67, 0x2d, 0x6e, 0x61, 0x6d, 0x65, 0x2e,
|
||||||
|
0x72, 0x75,
|
||||||
|
}
|
||||||
|
got, err := decodeMTProxySecret(hexSecret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("decoded bytes mismatch:\n got=%x\nwant=%x", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := decodeMTProxySecret(" " + hexSecret + "\n"); err != nil {
|
||||||
|
t.Fatalf("whitespace should be tolerated: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := decodeMTProxySecret(""); err == nil {
|
||||||
|
t.Fatal("expected error for empty secret")
|
||||||
|
}
|
||||||
|
if _, err := decodeMTProxySecret("not-hex!!"); err == nil {
|
||||||
|
t.Fatal("expected error for non-hex secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
|
||||||
|
// Copyright (C) 2026 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package upgrades
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.mau.fi/util/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Table.Register(-1, 9, 2, "Fix bug in legacy migration", dbutil.TxnModeOn, func(ctx context.Context, db *dbutil.Database) error {
|
||||||
|
if db.Dialect != dbutil.SQLite {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
exists, err := db.TableExists(ctx, "new_mx_room_state")
|
||||||
|
if !exists || err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = db.Exec(ctx, `
|
||||||
|
DROP TABLE mx_room_state;
|
||||||
|
ALTER TABLE new_mx_room_state RENAME TO mx_room_state;
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
+36
-26
@@ -257,6 +257,12 @@ func (tc *TelegramClient) convertToMatrix(
|
|||||||
log.Warn().Type("reply_to", replyTo).Msg("unhandled reply to type")
|
log.Warn().Type("reply_to", replyTo).Msg("unhandled reply to type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if cm.Parts[0].Extra == nil {
|
||||||
|
cm.Parts[0].Extra = make(map[string]any)
|
||||||
|
}
|
||||||
|
if externalURL := getMessageLink(msg); externalURL != "" {
|
||||||
|
cm.Parts[0].Extra["external_url"] = externalURL
|
||||||
|
}
|
||||||
if len(cm.Parts) > 1 {
|
if len(cm.Parts) > 1 {
|
||||||
log.Warn().Int("part_count", len(cm.Parts)).Msg("Message has multiple parts")
|
log.Warn().Int("part_count", len(cm.Parts)).Msg("Message has multiple parts")
|
||||||
for i, part := range cm.Parts[1:] {
|
for i, part := range cm.Parts[1:] {
|
||||||
@@ -274,6 +280,23 @@ func (tc *TelegramClient) convertToMatrix(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getMessageLink(msg *tg.Message) string {
|
||||||
|
var chatID int64
|
||||||
|
switch peer := msg.PeerID.(type) {
|
||||||
|
case *tg.PeerChat:
|
||||||
|
chatID = peer.ChatID
|
||||||
|
case *tg.PeerChannel:
|
||||||
|
chatID = peer.ChannelID
|
||||||
|
default: // also PeerUser
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
topicID := rawGetTopicID(msg.ReplyTo)
|
||||||
|
if topicID > 0 {
|
||||||
|
return fmt.Sprintf("https://t.me/c/%d/%d/%d", chatID, topicID, msg.ID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msg.ID)
|
||||||
|
}
|
||||||
|
|
||||||
func (tc *TelegramClient) addForwardHeader(ctx context.Context, part *bridgev2.ConvertedMessagePart, fwd tg.MessageFwdHeader) error {
|
func (tc *TelegramClient) addForwardHeader(ctx context.Context, part *bridgev2.ConvertedMessagePart, fwd tg.MessageFwdHeader) error {
|
||||||
var fwdFromText, fwdFromHTML string
|
var fwdFromText, fwdFromHTML string
|
||||||
switch from := fwd.FromID.(type) {
|
switch from := fwd.FromID.(type) {
|
||||||
@@ -584,11 +607,10 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
|
|||||||
if a.RoundMessage {
|
if a.RoundMessage {
|
||||||
extraInfo["fi.mau.telegram.round_message"] = a.RoundMessage
|
extraInfo["fi.mau.telegram.round_message"] = a.RoundMessage
|
||||||
}
|
}
|
||||||
extraInfo["duration"] = int(a.Duration * 1000)
|
|
||||||
case *tg.DocumentAttributeAudio:
|
case *tg.DocumentAttributeAudio:
|
||||||
if content.MsgType != event.MsgVideo {
|
if content.MsgType != event.MsgVideo {
|
||||||
content.MsgType = event.MsgAudio
|
content.MsgType = event.MsgAudio
|
||||||
extraInfo["duration"] = int(a.Duration * 1000) // only set the duration is not already set by the video handling logic
|
transferer = transferer.WithAudio(a) // only set the duration is not already set by the video handling logic
|
||||||
}
|
}
|
||||||
content.MSC1767Audio = &event.MSC1767Audio{
|
content.MSC1767Audio = &event.MSC1767Audio{
|
||||||
Duration: a.Duration * 1000,
|
Duration: a.Duration * 1000,
|
||||||
@@ -606,7 +628,7 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
|
|||||||
}
|
}
|
||||||
case *tg.DocumentAttributeImageSize:
|
case *tg.DocumentAttributeImageSize:
|
||||||
transferer = transferer.WithImageSize(a)
|
transferer = transferer.WithImageSize(a)
|
||||||
if content.MsgType == event.MsgFile {
|
if content.MsgType == event.MsgFile && !isSticker {
|
||||||
content.MsgType = event.MsgImage
|
content.MsgType = event.MsgImage
|
||||||
extra["fi.mau.telegram.force_document"] = true
|
extra["fi.mau.telegram.force_document"] = true
|
||||||
defaultFileName = "image_document"
|
defaultFileName = "image_document"
|
||||||
@@ -619,20 +641,9 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
|
|||||||
content.FileName = content.Body
|
content.FileName = content.Body
|
||||||
content.Body = a.Alt
|
content.Body = a.Alt
|
||||||
}
|
}
|
||||||
stickerInfo := map[string]any{"alt": a.Alt, "id": strconv.FormatInt(document.ID, 10)}
|
transferer = transferer.
|
||||||
|
WithStickerConfig(tc.main.Config.AnimatedSticker).
|
||||||
if setID, ok := a.Stickerset.(*tg.InputStickerSetID); ok {
|
WithStickerMetadata(tc.stickerSourceFromAttribute(ctx, document.ID, a))
|
||||||
stickerInfo["pack"] = map[string]any{
|
|
||||||
"id": strconv.FormatInt(setID.ID, 10),
|
|
||||||
"access_hash": strconv.FormatInt(setID.AccessHash, 10),
|
|
||||||
}
|
|
||||||
} else if shortName, ok := a.Stickerset.(*tg.InputStickerSetShortName); ok {
|
|
||||||
stickerInfo["pack"] = map[string]any{
|
|
||||||
"short_name": shortName.ShortName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
extraInfo["fi.mau.telegram.sticker"] = stickerInfo
|
|
||||||
transferer = transferer.WithStickerConfig(tc.main.Config.AnimatedSticker)
|
|
||||||
case *tg.DocumentAttributeAnimated:
|
case *tg.DocumentAttributeAnimated:
|
||||||
isVideoGif = true
|
isVideoGif = true
|
||||||
extraInfo["fi.mau.telegram.gif"] = true
|
extraInfo["fi.mau.telegram.gif"] = true
|
||||||
@@ -662,14 +673,6 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isVideoGif {
|
|
||||||
extraInfo["fi.mau.gif"] = true
|
|
||||||
extraInfo["fi.mau.loop"] = true
|
|
||||||
extraInfo["fi.mau.autoplay"] = true
|
|
||||||
extraInfo["fi.mau.hide_controls"] = true
|
|
||||||
extraInfo["fi.mau.no_audio"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := document.GetThumbs(); ok && eventType != event.EventSticker {
|
if _, ok := document.GetThumbs(); ok && eventType != event.EventSticker {
|
||||||
var thumbnailURL id.ContentURIString
|
var thumbnailURL id.ContentURIString
|
||||||
var thumbnailFile *event.EncryptedFileInfo
|
var thumbnailFile *event.EncryptedFileInfo
|
||||||
@@ -751,6 +754,13 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
|
|||||||
content.FileName = content.FileName + exmime.ExtensionFromMimetype(content.Info.MimeType)
|
content.FileName = content.FileName + exmime.ExtensionFromMimetype(content.Info.MimeType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if isVideoGif {
|
||||||
|
content.Info.MauGIF = true
|
||||||
|
extraInfo["fi.mau.loop"] = true
|
||||||
|
extraInfo["fi.mau.autoplay"] = true
|
||||||
|
extraInfo["fi.mau.hide_controls"] = true
|
||||||
|
extraInfo["fi.mau.no_audio"] = true
|
||||||
|
}
|
||||||
|
|
||||||
// Handle spoilers
|
// Handle spoilers
|
||||||
// See: https://github.com/matrix-org/matrix-spec-proposals/pull/3725
|
// See: https://github.com/matrix-org/matrix-spec-proposals/pull/3725
|
||||||
@@ -762,7 +772,7 @@ func (tc *TelegramClient) convertMediaRequiringUpload(
|
|||||||
extraInfo["fi.mau.telegram.spoiler"] = true
|
extraInfo["fi.mau.telegram.spoiler"] = true
|
||||||
}
|
}
|
||||||
if len(extraInfo) > 0 {
|
if len(extraInfo) > 0 {
|
||||||
extra["info"] = extraInfo
|
content.Info.Extra = extraInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
converted = &bridgev2.ConvertedMessagePart{
|
converted = &bridgev2.ConvertedMessagePart{
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package faketls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestReadSkipsChangeCipherSpec ensures interleaved ChangeCipherSpec records
|
||||||
|
// do not pollute the Application-data stream. Earlier behaviour wrote the
|
||||||
|
// 1-byte CCS payload into readBuf, which desynced the obfuscated2 CTR
|
||||||
|
// keystream and produced "msg_key is invalid" on decrypted MTProto messages.
|
||||||
|
func TestReadSkipsChangeCipherSpec(t *testing.T) {
|
||||||
|
a := require.New(t)
|
||||||
|
|
||||||
|
wire := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
// CCS record (1 byte = 0x01)
|
||||||
|
_, err := writeRecord(wire, record{
|
||||||
|
Type: RecordTypeChangeCipherSpec,
|
||||||
|
Version: Version12Bytes,
|
||||||
|
Data: []byte{0x01},
|
||||||
|
})
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Application record carrying our payload
|
||||||
|
payload := []byte("hello-mtproto-bytes")
|
||||||
|
_, err = writeRecord(wire, record{
|
||||||
|
Type: RecordTypeApplication,
|
||||||
|
Version: Version12Bytes,
|
||||||
|
Data: payload,
|
||||||
|
})
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Another CCS in the middle
|
||||||
|
_, err = writeRecord(wire, record{
|
||||||
|
Type: RecordTypeChangeCipherSpec,
|
||||||
|
Version: Version12Bytes,
|
||||||
|
Data: []byte{0x01},
|
||||||
|
})
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Second application record
|
||||||
|
more := []byte("second-payload")
|
||||||
|
_, err = writeRecord(wire, record{
|
||||||
|
Type: RecordTypeApplication,
|
||||||
|
Version: Version12Bytes,
|
||||||
|
Data: more,
|
||||||
|
})
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
tls := NewFakeTLS(zeroReader{}, &readonly{r: wire})
|
||||||
|
|
||||||
|
got, err := io.ReadAll(io.LimitReader(tls, int64(len(payload)+len(more))))
|
||||||
|
a.NoError(err)
|
||||||
|
a.Equal(append(append([]byte(nil), payload...), more...), got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readonly adapts an io.Reader to io.ReadWriter (NewFakeTLS demands one).
|
||||||
|
type readonly struct{ r io.Reader }
|
||||||
|
|
||||||
|
func (r *readonly) Read(p []byte) (int, error) { return r.r.Read(p) }
|
||||||
|
func (r *readonly) Write(p []byte) (int, error) { return len(p), nil }
|
||||||
@@ -14,7 +14,26 @@ import (
|
|||||||
|
|
||||||
const clientHelloLength = 517
|
const clientHelloLength = 517
|
||||||
|
|
||||||
func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32]byte) (randomOffset int) {
|
// generateGrease produces seven GREASE bytes following the TLS spec
|
||||||
|
// (RFC 8701) and tdlib's TlsInit.cpp constraints used by MTProxy faketls
|
||||||
|
// validators: each byte has the form 0x?A (low nibble 0x0A), and grease[3]
|
||||||
|
// must differ from grease[4].
|
||||||
|
func generateGrease(rng io.Reader) ([7]byte, error) {
|
||||||
|
var raw [7]byte
|
||||||
|
if _, err := io.ReadFull(rng, raw[:]); err != nil {
|
||||||
|
return raw, errors.Wrap(err, "read grease entropy")
|
||||||
|
}
|
||||||
|
var g [7]byte
|
||||||
|
for i, r := range raw {
|
||||||
|
g[i] = (r & 0xF0) | 0x0A
|
||||||
|
}
|
||||||
|
if g[3] == g[4] {
|
||||||
|
g[3] ^= 0x10
|
||||||
|
}
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32]byte, grease [7]byte) (randomOffset int) {
|
||||||
S := func(s string) {
|
S := func(s string) {
|
||||||
b.Buf = append(b.Buf, s...)
|
b.Buf = append(b.Buf, s...)
|
||||||
}
|
}
|
||||||
@@ -22,8 +41,9 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32
|
|||||||
randomOffset = len(b.Buf)
|
randomOffset = len(b.Buf)
|
||||||
b.Expand(n)
|
b.Expand(n)
|
||||||
}
|
}
|
||||||
G := func(_ int) {
|
G := func(n int) {
|
||||||
b.Expand(0)
|
v := grease[n]
|
||||||
|
b.Buf = append(b.Buf, v, v)
|
||||||
}
|
}
|
||||||
R := func() {
|
R := func() {
|
||||||
b.Buf = append(b.Buf, sessionID[:]...)
|
b.Buf = append(b.Buf, sessionID[:]...)
|
||||||
@@ -83,9 +103,18 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32
|
|||||||
G(3)
|
G(3)
|
||||||
S("\x00\x01\x00\x00\x15")
|
S("\x00\x01\x00\x00\x15")
|
||||||
|
|
||||||
if pad := clientHelloLength - b.Len(); pad > 0 {
|
// Padding extension (id 0x0015 already written above): write its
|
||||||
b.Expand(pad)
|
// length so the resulting ClientHello is exactly clientHelloLength
|
||||||
|
// bytes, then fill the body with zeros.
|
||||||
|
padLen := clientHelloLength - b.Len() - 2
|
||||||
|
if padLen < 0 {
|
||||||
|
padLen = 0
|
||||||
}
|
}
|
||||||
|
lenPos := b.Len()
|
||||||
|
b.Expand(2)
|
||||||
|
binary.BigEndian.PutUint16(b.Buf[lenPos:lenPos+2], uint16(padLen))
|
||||||
|
b.Expand(padLen)
|
||||||
|
|
||||||
return randomOffset
|
return randomOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,15 +123,21 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32
|
|||||||
// See https://tools.ietf.org/html/rfc5246#section-7.4.1.1.
|
// See https://tools.ietf.org/html/rfc5246#section-7.4.1.1.
|
||||||
func writeClientHello(
|
func writeClientHello(
|
||||||
w io.Writer,
|
w io.Writer,
|
||||||
|
rng io.Reader,
|
||||||
now clock.Clock,
|
now clock.Clock,
|
||||||
sessionID [32]byte,
|
sessionID [32]byte,
|
||||||
domain string,
|
domain string,
|
||||||
secret []byte,
|
secret []byte,
|
||||||
) (r [32]byte, err error) {
|
) (r [32]byte, err error) {
|
||||||
|
grease, err := generateGrease(rng)
|
||||||
|
if err != nil {
|
||||||
|
return [32]byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
b := &bin.Buffer{
|
b := &bin.Buffer{
|
||||||
Buf: make([]byte, 0, 576),
|
Buf: make([]byte, 0, 576),
|
||||||
}
|
}
|
||||||
randomOffset := createClientHello(b, sessionID, domain, [32]byte{})
|
randomOffset := createClientHello(b, sessionID, domain, [32]byte{}, grease)
|
||||||
|
|
||||||
// https://github.com/tdlib/td/blob/27d3fdd09d90f6b77ecbcce50b1e86dc4b3dd366/td/mtproto/TlsInit.cpp#L380-L384
|
// https://github.com/tdlib/td/blob/27d3fdd09d90f6b77ecbcce50b1e86dc4b3dd366/td/mtproto/TlsInit.cpp#L380-L384
|
||||||
mac := hmac.New(sha256.New, secret)
|
mac := hmac.New(sha256.New, secret)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func (o *FakeTLS) Handshake(protocol [4]byte, dc int, s mtproxy.Secret) error {
|
|||||||
return errors.Wrap(err, "generate sessionID")
|
return errors.Wrap(err, "generate sessionID")
|
||||||
}
|
}
|
||||||
|
|
||||||
clientDigest, err := writeClientHello(o.conn, o.clock, sessionID, s.CloakHost, s.Secret)
|
clientDigest, err := writeClientHello(o.conn, o.rand, o.clock, sessionID, s.CloakHost, s.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "send ClientHello")
|
return errors.Wrap(err, "send ClientHello")
|
||||||
}
|
}
|
||||||
@@ -93,20 +93,27 @@ func (o *FakeTLS) Read(b []byte) (n int, err error) {
|
|||||||
return o.readBuf.Read(b)
|
return o.readBuf.Read(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
rec, err := readRecord(o.conn)
|
// Skip ChangeCipherSpec records — they are TLS-level keep-alive /
|
||||||
if err != nil {
|
// compatibility markers (one-byte payload 0x01) and must NOT be
|
||||||
return 0, errors.Wrap(err, "read TLS record")
|
// passed up to the obfuscated2 layer, otherwise the CTR keystream
|
||||||
}
|
// position desyncs and subsequent MTProto messages decrypt to
|
||||||
|
// garbage (`msg_key is invalid`).
|
||||||
|
for {
|
||||||
|
rec, err := readRecord(o.conn)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "read TLS record")
|
||||||
|
}
|
||||||
|
|
||||||
switch rec.Type {
|
switch rec.Type {
|
||||||
case RecordTypeChangeCipherSpec:
|
case RecordTypeChangeCipherSpec:
|
||||||
case RecordTypeApplication:
|
continue
|
||||||
case RecordTypeHandshake:
|
case RecordTypeApplication:
|
||||||
return 0, errors.New("unexpected record type handshake")
|
o.readBuf.Write(rec.Data)
|
||||||
default:
|
return o.readBuf.Read(b)
|
||||||
return 0, errors.Errorf("unsupported record type %v", rec.Type)
|
case RecordTypeHandshake:
|
||||||
|
return 0, errors.New("unexpected record type handshake")
|
||||||
|
default:
|
||||||
|
return 0, errors.Errorf("unsupported record type %v", rec.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
o.readBuf.Write(rec.Data)
|
|
||||||
|
|
||||||
return o.readBuf.Read(b)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import (
|
|||||||
"github.com/gotd/neo"
|
"github.com/gotd/neo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type zeroReader struct{}
|
||||||
|
|
||||||
|
func (zeroReader) Read(p []byte) (int, error) {
|
||||||
|
for i := range p {
|
||||||
|
p[i] = 0
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestTLS(t *testing.T) {
|
func TestTLS(t *testing.T) {
|
||||||
a := require.New(t)
|
a := require.New(t)
|
||||||
secret := [32]byte{}
|
secret := [32]byte{}
|
||||||
@@ -17,42 +26,42 @@ func TestTLS(t *testing.T) {
|
|||||||
c := neo.NewTime(time.Date(2010, 10, 10, 1, 1, 1, 0, time.UTC))
|
c := neo.NewTime(time.Date(2010, 10, 10, 1, 1, 1, 0, time.UTC))
|
||||||
|
|
||||||
b := bytes.NewBuffer(nil)
|
b := bytes.NewBuffer(nil)
|
||||||
_, err := writeClientHello(b, c, sessionID, "google.com", secret[:])
|
_, err := writeClientHello(b, zeroReader{}, c, sessionID, "google.com", secret[:])
|
||||||
a.NoError(err)
|
a.NoError(err)
|
||||||
|
|
||||||
testVector := []byte{
|
testVector := []byte{
|
||||||
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0xf9, 0x75, 0x5f, 0xdd, 0xb9,
|
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0xa9, 0xa8, 0x7f, 0x37, 0x9b,
|
||||||
0xe3, 0x46, 0x57, 0x5a, 0x26, 0x71, 0xfa, 0x29, 0x7f, 0xab, 0xf0, 0xa1, 0xf3, 0x69, 0x4f, 0x72,
|
0x09, 0x80, 0x6a, 0xf3, 0xff, 0x78, 0x4a, 0x6c, 0x4e, 0xbd, 0xdd, 0x94, 0x31, 0x8e, 0x7c, 0x09,
|
||||||
0xe0, 0xc3, 0x8f, 0x62, 0x77, 0x5c, 0x8f, 0x5a, 0xf8, 0xa2, 0xa9, 0x20, 0x00, 0x00, 0x00, 0x00,
|
0x36, 0x63, 0x77, 0x1d, 0x36, 0xf4, 0xcb, 0x6d, 0x3e, 0x13, 0x83, 0x20, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x13, 0x01,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0a, 0x0a,
|
||||||
0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, 0xcc, 0xa8,
|
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9,
|
||||||
0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93,
|
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0d, 0x00, 0x00, 0x0a, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
0x01, 0x93, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0d, 0x00, 0x00, 0x0a, 0x67,
|
||||||
0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a,
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00,
|
||||||
0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00,
|
0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x0a, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18,
|
||||||
0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74,
|
0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c,
|
||||||
0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05,
|
||||||
0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05,
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04,
|
||||||
0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x2b, 0x00, 0x29, 0x00,
|
0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00,
|
||||||
0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x33, 0x00, 0x2b, 0x00, 0x29, 0x0a, 0x0a, 0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d,
|
||||||
0x0a, 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02,
|
0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x03, 0x03, 0x03,
|
||||||
0x00, 0x01, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02, 0x1a, 0x1a, 0x00, 0x01, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x15, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
}
|
}
|
||||||
a.Equal(testVector, b.Bytes())
|
a.Equal(testVector, b.Bytes())
|
||||||
|
|||||||
@@ -4,11 +4,20 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/go-faster/errors"
|
"github.com/go-faster/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// peekDump returns up to n bytes from the start of buf as a hex string for diagnostics.
|
||||||
|
func peekDump(buf []byte, n int) string {
|
||||||
|
if len(buf) < n {
|
||||||
|
n = len(buf)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(buf[:n])
|
||||||
|
}
|
||||||
|
|
||||||
// readServerHello reads faketls ServerHello.
|
// readServerHello reads faketls ServerHello.
|
||||||
func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
||||||
packetBuf := bytes.NewBuffer(nil)
|
packetBuf := bytes.NewBuffer(nil)
|
||||||
@@ -16,10 +25,11 @@ func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
|||||||
|
|
||||||
handshake, err := readRecord(r)
|
handshake, err := readRecord(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "handshake record")
|
return errors.Wrapf(err, "handshake record (peek=%s)", peekDump(packetBuf.Bytes(), 32))
|
||||||
}
|
}
|
||||||
if handshake.Type != RecordTypeHandshake {
|
if handshake.Type != RecordTypeHandshake {
|
||||||
return errors.Wrap(err, "unexpected record type")
|
return errors.Errorf("unexpected handshake record type: got 0x%02x, want 0x%02x (peek=%s)",
|
||||||
|
byte(handshake.Type), byte(RecordTypeHandshake), peekDump(packetBuf.Bytes(), 32))
|
||||||
}
|
}
|
||||||
|
|
||||||
changeCipher, err := readRecord(r)
|
changeCipher, err := readRecord(r)
|
||||||
@@ -27,7 +37,8 @@ func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
|||||||
return errors.Wrap(err, "change cipher record")
|
return errors.Wrap(err, "change cipher record")
|
||||||
}
|
}
|
||||||
if changeCipher.Type != RecordTypeChangeCipherSpec {
|
if changeCipher.Type != RecordTypeChangeCipherSpec {
|
||||||
return errors.Wrap(err, "unexpected record type")
|
return errors.Errorf("unexpected change cipher record type: got 0x%02x, want 0x%02x",
|
||||||
|
byte(changeCipher.Type), byte(RecordTypeChangeCipherSpec))
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := readRecord(r)
|
cert, err := readRecord(r)
|
||||||
@@ -35,7 +46,8 @@ func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
|||||||
return errors.Wrap(err, "cert record")
|
return errors.Wrap(err, "cert record")
|
||||||
}
|
}
|
||||||
if cert.Type != RecordTypeApplication {
|
if cert.Type != RecordTypeApplication {
|
||||||
return errors.Wrap(err, "unexpected record type")
|
return errors.Errorf("unexpected application record type: got 0x%02x, want 0x%02x",
|
||||||
|
byte(cert.Type), byte(RecordTypeApplication))
|
||||||
}
|
}
|
||||||
|
|
||||||
// `$record_header = type 1 byte + version 2 bytes + payload_length 2 bytes = 5 bytes`
|
// `$record_header = type 1 byte + version 2 bytes + payload_length 2 bytes = 5 bytes`
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package faketls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestClientHelloStructure verifies that what we generate is a syntactically
|
||||||
|
// valid TLS ClientHello — i.e., the Go crypto/tls server can parse it without
|
||||||
|
// returning a "decode_error"-like syntax error. We don't care that the TLS
|
||||||
|
// handshake then fails (it will, since we're using a fake cert / random data);
|
||||||
|
// we only care that parsing succeeds.
|
||||||
|
func TestClientHelloStructure(t *testing.T) {
|
||||||
|
// Render with deterministic rng + sessionID + key.
|
||||||
|
var session [32]byte
|
||||||
|
for i := range session {
|
||||||
|
session[i] = byte(i)
|
||||||
|
}
|
||||||
|
var key [32]byte
|
||||||
|
for i := range key {
|
||||||
|
key[i] = 0xAA
|
||||||
|
}
|
||||||
|
var grease [7]byte
|
||||||
|
for i := range grease {
|
||||||
|
grease[i] = byte(0x0A + i*0x10)
|
||||||
|
}
|
||||||
|
if grease[3] == grease[4] {
|
||||||
|
grease[3] ^= 0x10
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &bin.Buffer{Buf: make([]byte, 0, 576)}
|
||||||
|
createClientHello(b, session, "example.com", key, grease)
|
||||||
|
if len(b.Buf) != clientHelloLength {
|
||||||
|
t.Fatalf("expected %d bytes, got %d", clientHelloLength, len(b.Buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire it through a real TLS server. The server reads bytes from
|
||||||
|
// our pipe; if it accepts ClientHello but fails on cert/MAC, we get
|
||||||
|
// a non-syntax error. If it returns "decode_error", we know we're
|
||||||
|
// still busted.
|
||||||
|
clientConn, serverConn := net.Pipe()
|
||||||
|
defer clientConn.Close()
|
||||||
|
defer serverConn.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
clientConn.Write(b.Buf)
|
||||||
|
// keep the pipe open until done
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{generateSelfSigned(t)},
|
||||||
|
}
|
||||||
|
srv := tls.Server(serverConn, cfg)
|
||||||
|
srv.SetDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
err := srv.Handshake()
|
||||||
|
if err == nil {
|
||||||
|
return // unexpectedly succeeded — fine for our purpose
|
||||||
|
}
|
||||||
|
t.Logf("server handshake error (expected non-syntax): %v", err)
|
||||||
|
|
||||||
|
msg := err.Error()
|
||||||
|
for _, marker := range []string{"decode_error", "syntax", "malformed", "bad ClientHello"} {
|
||||||
|
if strings.Contains(msg, marker) {
|
||||||
|
t.Fatalf("structural parse failure (%q) — ClientHello is malformed: %v", marker, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSelfSigned builds a throwaway cert for the test TLS server.
|
||||||
|
func generateSelfSigned(t *testing.T) tls.Certificate {
|
||||||
|
cert, err := tls.X509KeyPair(testCertPEM, testKeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated with `go run filippo.io/mkcert@latest -ecdsa example.com`-ish.
|
||||||
|
// Embedded here for deterministic test environment.
|
||||||
|
var testCertPEM = []byte(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
|
||||||
|
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
|
||||||
|
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
|
||||||
|
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
|
||||||
|
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
|
||||||
|
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
|
||||||
|
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
|
||||||
|
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
|
||||||
|
6MF9+Yw1Yy0t
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
|
||||||
|
var testKeyPEM = []byte(`-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q
|
||||||
|
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
|
||||||
|
-----END EC PRIVATE KEY-----`)
|
||||||
@@ -53,7 +53,12 @@ func (o *Obfuscated2) Read(b []byte) (int, error) {
|
|||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
o.decrypt.XORKeyStream(b, b)
|
// IMPORTANT: only XOR the n bytes that were actually read.
|
||||||
|
// XOR-ing the full b advances the CTR keystream past where the
|
||||||
|
// server is and permanently desyncs the stream — every later
|
||||||
|
// MTProto message decrypts to garbage and the engine fails
|
||||||
|
// with "msg_key is invalid".
|
||||||
|
o.decrypt.XORKeyStream(b[:n], b[:n])
|
||||||
}
|
}
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package obfuscated2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// chunkConn delivers data from buf in chunks of at most chunkSize bytes.
|
||||||
|
type chunkConn struct {
|
||||||
|
buf *bytes.Buffer
|
||||||
|
chunkSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chunkConn) Read(p []byte) (int, error) {
|
||||||
|
if c.buf.Len() == 0 {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
want := len(p)
|
||||||
|
if want > c.chunkSize {
|
||||||
|
want = c.chunkSize
|
||||||
|
}
|
||||||
|
return c.buf.Read(p[:want])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chunkConn) Write(p []byte) (int, error) { return len(p), nil }
|
||||||
|
|
||||||
|
// TestShortReadKeepsKeystreamAligned ensures that when the underlying
|
||||||
|
// transport returns fewer bytes than the caller asked for, the CTR
|
||||||
|
// keystream is only advanced by the bytes actually delivered.
|
||||||
|
//
|
||||||
|
// The previous implementation called XORKeyStream(b, b) instead of
|
||||||
|
// XORKeyStream(b[:n], b[:n]); after a single short read the client and
|
||||||
|
// server keystreams diverged and every subsequent MTProto message
|
||||||
|
// failed integrity (msg_key invalid).
|
||||||
|
func TestShortReadKeepsKeystreamAligned(t *testing.T) {
|
||||||
|
a := require.New(t)
|
||||||
|
|
||||||
|
key := bytes.Repeat([]byte{0x11}, 32)
|
||||||
|
iv := bytes.Repeat([]byte{0x22}, 16)
|
||||||
|
|
||||||
|
enc, err := aes.NewCipher(key)
|
||||||
|
a.NoError(err)
|
||||||
|
dec, err := aes.NewCipher(key)
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
encStream := cipher.NewCTR(enc, iv)
|
||||||
|
decStream := cipher.NewCTR(dec, iv)
|
||||||
|
|
||||||
|
plaintext := bytes.Repeat([]byte("Hello, MTProxy! "), 50)
|
||||||
|
ciphertext := make([]byte, len(plaintext))
|
||||||
|
encStream.XORKeyStream(ciphertext, plaintext)
|
||||||
|
|
||||||
|
wire := &chunkConn{buf: bytes.NewBuffer(append([]byte(nil), ciphertext...)), chunkSize: 7}
|
||||||
|
o := &Obfuscated2{
|
||||||
|
conn: wire,
|
||||||
|
keys: keys{decrypt: decStream},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := make([]byte, len(plaintext))
|
||||||
|
off := 0
|
||||||
|
for off < len(plaintext) {
|
||||||
|
end := off + 128
|
||||||
|
if end > len(got) {
|
||||||
|
end = len(got)
|
||||||
|
}
|
||||||
|
n, err := o.Read(got[off:end])
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
t.Fatalf("read at off %d: %v", off, err)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
t.Fatalf("zero-length read at off %d", off)
|
||||||
|
}
|
||||||
|
off += n
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Equal(plaintext, got, "short reads must not desync the keystream")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user