move gotd fork into repo. (#111)

- update to latest telegram layer
- remove some references to fields in tg.Entities that don't exist in
the schema
- originally added here:
https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e
  - referenced here
-
https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3
-
https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
This commit is contained in:
Adam Van Ymeren
2025-06-27 20:03:37 -07:00
committed by GitHub
parent 0952df0244
commit 7a04f298d2
19264 changed files with 1539697 additions and 84 deletions
@@ -0,0 +1,191 @@
// Package deeplink contains deeplink parsing helpers.
package deeplink
import (
"net/url"
"strings"
"github.com/go-faster/errors"
)
// Type is an enum type of Telegram deeplinks types.
type Type string
const (
// Resolve is deeplink like
//
// tg:resolve?domain={domain}
// tg://resolve?domain={domain}
// https://t.me/{domain}
// https://telegram.me/{domain}
//
Resolve Type = "resolve"
// Join is deeplink like
//
// tg:join?invite={hash}
// tg://join?invite={hash}
// https://t.me/joinchat/{hash}
// https://telegram.me/joinchat/{hash}
// t.me/+{hash}
//
Join Type = "join"
)
// DeepLink represents Telegram deeplink.
type DeepLink struct {
Type Type
Args url.Values
}
func ensureParam(query url.Values, key string) error {
if query.Get(key) == "" {
return errors.Errorf("should have %q query parameter", key)
}
return nil
}
func (d DeepLink) validate() error {
switch d.Type {
case Resolve:
return ensureParam(d.Args, "domain")
case Join:
return ensureParam(d.Args, "invite")
default:
return errors.Errorf("unsupported deeplink %q", d.Type)
}
}
func parseTg(u *url.URL) (DeepLink, error) {
query := u.Query()
switch Type(u.Hostname()) {
case Resolve:
return DeepLink{
Type: Resolve,
Args: query,
}, nil
case Join:
return DeepLink{
Type: Join,
Args: query,
}, nil
}
return DeepLink{}, errors.Errorf("unsupported deeplink %q", u.String())
}
func parseHTTPS(u *url.URL) (DeepLink, error) {
cleanInviteHash := func(root string) string {
hash := strings.Trim(root, "+ ")
if u.RawPath == "" {
hash = url.PathEscape(hash)
}
return hash
}
query := url.Values{}
p := strings.TrimPrefix(u.Path, "/")
p = strings.TrimSuffix(p, "/")
split := strings.Split(p, "/")
var (
root = split[0]
base string
)
if len(split) > 1 {
base = split[1]
}
switch root {
case "joinchat":
query.Set("invite", cleanInviteHash(base))
return DeepLink{
Type: Join,
Args: query,
}, nil
case "":
return DeepLink{}, errors.Errorf("unsupported deeplink %q", u.String())
}
switch root[0] {
case ' ', '+':
query.Set("invite", cleanInviteHash(root))
return DeepLink{
Type: Join,
Args: query,
}, nil
default:
if err := ValidateDomain(root); err != nil {
return DeepLink{}, err
}
query.Set("domain", root)
return DeepLink{
Type: Resolve,
Args: query,
}, nil
}
}
func hasTelegramPrefix(link string) bool {
return strings.HasPrefix(link, "t.me") ||
strings.HasPrefix(link, "telegram.me") ||
strings.HasPrefix(link, "telegram.dog")
}
// IsDeeplinkLike returns true if string may be a valid deeplink.
func IsDeeplinkLike(link string) bool {
return strings.HasPrefix(link, "tg:") ||
hasTelegramPrefix(link) ||
strings.HasPrefix(link, "https://")
}
// Parse parses and returns deeplink.
func Parse(link string) (DeepLink, error) {
switch {
// Normalize case like t.me/gotd.
case hasTelegramPrefix(link):
link = strings.TrimSuffix("https://"+link, "/")
// Normalize case like tg:resolve?domain=gotd.
case !strings.HasPrefix(link, "tg://") && strings.HasPrefix(link, "tg:"):
link = "tg://" + strings.TrimPrefix(link, "tg:")
}
u, err := url.Parse(link)
if err != nil {
return DeepLink{}, errors.Wrapf(err, "invalid URL %q", link)
}
var d DeepLink
switch {
case u.Scheme == "https":
switch strings.TrimPrefix(u.Hostname(), "www.") {
case "t.me", "telegram.me", "telegram.dog":
d, err = parseHTTPS(u)
default:
return DeepLink{}, errors.Errorf("invalid domain %q", link)
}
case u.Scheme == "tg":
d, err = parseTg(u)
default:
return DeepLink{}, errors.Errorf("invalid deeplink %q", link)
}
if err != nil {
return DeepLink{}, err
}
if err := d.validate(); err != nil {
return DeepLink{}, err
}
return d, nil
}
// Expect parses deeplink and check type its type.
func Expect(link string, typ Type) (DeepLink, error) {
l, err := Parse(link)
if err != nil {
return l, err
}
if l.Type != typ {
return l, errors.Errorf("unexpected deeplink type %q", l.Type)
}
return l, nil
}
@@ -0,0 +1,28 @@
//go:build go1.18
package deeplink
import (
"testing"
)
func addSuites(f *testing.F, suites map[string][]testCase) {
for _, suite := range suites {
for _, test := range suite {
f.Add(test.input)
}
}
}
func FuzzParse(f *testing.F) {
for _, typeSuite := range typeSuites {
addSuites(f, typeSuite)
}
f.Fuzz(func(t *testing.T, link string) {
_, err := Parse(link)
if err != nil {
t.Skip(err)
}
})
}
@@ -0,0 +1,158 @@
package deeplink
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
type testCase struct {
link DeepLink
input string
wantErr bool
}
func join(arg string) DeepLink {
return DeepLink{
Type: Join,
Args: map[string][]string{
"invite": {arg},
},
}
}
func resolve(arg string) DeepLink {
return DeepLink{
Type: Resolve,
Args: map[string][]string{
"domain": {arg},
},
}
}
func joinSuite() map[string][]testCase {
expect := join("AAAAAAAAAAAAAAAAAA")
return map[string][]testCase{
"Test": {
{expect, `t.me/joinchat/AAAAAAAAAAAAAAAAAA`, false},
{expect, `t.me/joinchat/AAAAAAAAAAAAAAAAAA/`, false},
{expect, `t.me/+AAAAAAAAAAAAAAAAAA`, false},
{expect, `t.me/+AAAAAAAAAAAAAAAAAA/`, false},
{expect, `t.me/ +AAAAAAAAAAAAAAAAAA/`, false},
{expect, `https://t.me/joinchat/AAAAAAAAAAAAAAAAAA`, false},
{expect, `https://t.me/joinchat/AAAAAAAAAAAAAAAAAA/`, false},
{expect, `tg:join?invite=AAAAAAAAAAAAAAAAAA`, false},
{expect, `tg://join?invite=AAAAAAAAAAAAAAAAAA`, false},
{DeepLink{}, `https://t.co/joinchat/AAAAAAAAAAAAAAAAAA`, true},
{DeepLink{}, `rt://join?invite=AAAAAAAAAAAAAAAAAA`, true},
},
"TDLib": {
// t.me/+<hash>
// Positive
{join("aba%20aba"), "t.me/+aba%20aba", false},
{join("aba0aba"), "t.me/+aba%30aba", false},
{join("123456a"), "t.me/+123456a", false},
{join("12345678901"), "t.me/%2012345678901", false},
// Negative
{DeepLink{}, "t.me/+?invite=abcdef", true},
{DeepLink{}, "t.me/+", true},
{DeepLink{}, "t.me/+/abcdef", true},
{DeepLink{}, "t.me/ ?/abcdef", true},
{DeepLink{}, "t.me/+?abcdef", true},
{DeepLink{}, "t.me/+#abcdef", true},
{DeepLink{}, "t.me/ /123456/123123/12/31/a/s//21w/?asdas#test", true},
// t.me/joinchat/<hash>
// Positive
{join("abacaba"), "t.me/joinchat/abacaba", false},
{join("aba%20aba"), "t.me/joinchat/aba%20aba", false},
{join("aba0aba"), "t.me/joinchat/aba%30aba", false},
{join("123456a"), "t.me/joinchat/123456a", false},
{join("12345678901"), "t.me/joinchat/12345678901", false},
{join("123456"), "t.me/joinchat/123456", false},
{join("123456"), "t.me/joinchat/123456/123123/12/31/a/s//21w/?asdas#test", false},
// Negative
{DeepLink{}, "t.me/joinchat?invite=abcdef", true},
{DeepLink{}, "t.me/joinchat", true},
{DeepLink{}, "t.me/joinchat/", true},
{DeepLink{}, "t.me/joinchat//abcdef", true},
{DeepLink{}, "t.me/joinchat?/abcdef", true},
{DeepLink{}, "t.me/joinchat/?abcdef", true},
{DeepLink{}, "t.me/joinchat/#abcdef", true},
},
}
}
func resolveSuite() map[string][]testCase {
expect := resolve("gotd_ru")
return map[string][]testCase{
"Test": {
{expect, `t.me/gotd_ru`, false},
{expect, `t.me/gotd_ru/`, false},
{expect, `https://t.me/gotd_ru`, false},
{expect, `https://t.me/gotd_ru/`, false},
{expect, `tg:resolve?domain=gotd_ru`, false},
{expect, `tg:resolve?&&&&&&&domain=gotd_ru`, false},
{expect, `tg://resolve?domain=gotd_ru`, false},
{DeepLink{}, `https://t.co/gotd_ru`, true},
{DeepLink{}, `rt://join?invite=AAAAAAAAAAAAAAAAAA`, true},
},
"TDLib": {
// t.me/<domain>
// Positive
{resolve("a"), "t.me/a", false},
{resolve("abcdefghijklmnopqrstuvwxyz123456"), "t.me/abcdefghijklmnopqrstuvwxyz123456", false},
{resolve("Aasdf"), "t.me/Aasdf", false},
{resolve("asdf0"), "t.me/asdf0", false},
{resolve("username"), "t.me/username/0/a//s/as?gam=asd", false},
{resolve("username"), "t.me/username/aasdas?test=1", false},
{resolve("username"), "t.me/username/0", false},
{resolve("telecram"), "https://telegram.dog/tele%63ram", false},
// Negative
{DeepLink{}, "t.me/abcdefghijklmnopqrstuvwxyz1234567", true},
{DeepLink{}, "t.me/abcdefghijklmnop-qrstuvwxyz", true},
{DeepLink{}, "t.me/abcdefghijklmnop~qrstuvwxyz", true},
{DeepLink{}, "t.me/_asdf", true},
{DeepLink{}, "t.me/0asdf", true},
{DeepLink{}, "t.me/9asdf", true},
{DeepLink{}, "t.me/asdf_", true},
{DeepLink{}, "t.me/asd__fg", true},
{DeepLink{}, "t.me//username", true},
},
}
}
var typeSuites = map[string]map[string][]testCase{
"Join": joinSuite(),
"Resolve": resolveSuite(),
}
func TestParseDeeplink(t *testing.T) {
runSuite := func(suite []testCase) func(t *testing.T) {
return func(t *testing.T) {
for i, test := range suite {
t.Run(fmt.Sprintf("Test%d (%s)", i, test.input), func(t *testing.T) {
a := require.New(t)
d, err := Parse(test.input)
if test.wantErr {
a.Error(err, test.input)
} else {
a.NoError(err, test.input)
a.Equal(test.link, d, test.input)
}
})
}
}
}
for typeName, typeSuite := range typeSuites {
t.Run(typeName, func(t *testing.T) {
for suiteName, suite := range typeSuite {
t.Run(suiteName, runSuite(suite))
}
})
}
}
@@ -0,0 +1,40 @@
package deeplink
import (
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/ascii"
)
// ValidateDomain validate given domain (user) name
func ValidateDomain(domain string) error {
return checkDomainSymbols(domain)
}
// checkDomainSymbols check that domain contains only a-z, A-Z, 0-9 and '_'
// symbols.
func checkDomainSymbols(domain string) error {
switch {
case domain == "":
return errors.New("is empty")
case len(domain) > 32:
return errors.New("is too big")
case !ascii.IsLatinLower(rune(domain[0])):
return errors.New("must start with lower letter")
case domain[len(domain)-1] == '_':
return errors.New("must not end with '_'")
}
for i, r := range domain {
switch {
case !ascii.IsLatinLetter(r) && !ascii.IsDigit(r) && r != '_':
case i > 0 && domain[i] == '_' && domain[i] == domain[i-1]:
default:
continue
}
return errors.Errorf("unexpected %c at %d", r, i)
}
return nil
}
@@ -0,0 +1,38 @@
package deeplink
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestValidateDomain(t *testing.T) {
tests := []struct {
domain string
wantErr bool
}{
{"a", false},
{"abcdefghijklmnopqrstuvwxyz123456", false},
{"Aasdf", false},
{"asdf0", false},
{"", true},
{"asdf_", true},
{"asd__fg", true},
{"_asdf", true},
{"0asdf", true},
{"9asdf", true},
{"abcdefghijklmnopqrstuvwxyz1234567", true},
{"abcdefghijklmnop-qrstuvwxyz", true},
{"abcdefghijklmnop~qrstuvwxyz", true},
}
for _, tt := range tests {
t.Run(tt.domain, func(t *testing.T) {
err := ValidateDomain(tt.domain)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}