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
+152
View File
@@ -0,0 +1,152 @@
// Package tgerr implements helpers for error handling.
package tgerr
import (
"fmt"
"strconv"
"strings"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/ascii"
)
// Error represents RPC error returned as result to request.
type Error struct {
Code int // 420
Message string // FLOOD_WAIT_3
Type string // FLOOD_WAIT
Argument int // 3
}
// New creates new *Error from code and message, extracting argument
// and type.
func New(code int, msg string) *Error {
e := &Error{
Code: code,
Message: msg,
}
e.extractArgument()
return e
}
// IsType reports whether error has type t.
func (e *Error) IsType(t string) bool {
if e == nil {
return false
}
return e.Type == t
}
// IsCode reports whether error Code is equal to code.
func (e *Error) IsCode(code int) bool {
if e == nil {
return false
}
return e.Code == code
}
// IsOneOf returns true if error type is in tt.
func (e *Error) IsOneOf(tt ...string) bool {
if e == nil {
return false
}
for _, t := range tt {
if e.IsType(t) {
return true
}
}
return false
}
// IsCodeOneOf returns true if error code is one of codes.
func (e *Error) IsCodeOneOf(codes ...int) bool {
if e == nil {
return false
}
for _, code := range codes {
if e.IsCode(code) {
return true
}
}
return false
}
// extractArgument extracts Type and Argument from Message.
func (e *Error) extractArgument() {
if e.Message == "" {
return
}
// Defaulting Type to Message.
e.Type = e.Message
// Splitting by underscore.
parts := strings.Split(e.Message, "_")
if len(parts) < 2 {
return
}
var nonDigit []string
Parts:
for _, part := range parts {
for _, r := range part {
if ascii.IsDigit(r) {
continue
}
// Found non-digit part, skipping.
nonDigit = append(nonDigit, part)
continue Parts
}
// Found digit-only part, using as argument.
argument, err := strconv.Atoi(part)
if err != nil {
// Should be unreachable.
return
}
e.Argument = argument
}
e.Type = strings.Join(nonDigit, "_")
}
func (e *Error) Error() string {
if e.Type != e.Message {
return fmt.Sprintf("rpc error code %d: %s (%d)", e.Code, e.Type, e.Argument)
}
return fmt.Sprintf("rpc error code %d: %s", e.Code, e.Message)
}
// AsType returns *Error from err if rpc error type is t.
func AsType(err error, t string) (rpcErr *Error, ok bool) {
if errors.As(err, &rpcErr) && rpcErr.Type == t {
return rpcErr, true
}
return nil, false
}
// As extracts *Error from err if possible.
func As(err error) (rpcErr *Error, ok bool) {
if errors.As(err, &rpcErr) {
return rpcErr, true
}
return nil, false
}
// Is returns true if err type is t.
func Is(err error, tt ...string) bool {
if rpcErr, ok := As(err); ok {
return rpcErr.IsOneOf(tt...)
}
return false
}
// IsCode returns true of error code is as provided.
func IsCode(err error, code ...int) bool {
if rpcErr, ok := As(err); ok {
return rpcErr.IsCodeOneOf(code...)
}
return false
}
+115
View File
@@ -0,0 +1,115 @@
package tgerr_test
import (
"testing"
"github.com/go-faster/errors"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
)
func TestError(t *testing.T) {
t.Run("FLOOD_WAIT_0", func(t *testing.T) {
require.Equal(t, "rpc error code 420: FLOOD_WAIT (0)", tgerr.New(420, "FLOOD_WAIT_0").Error())
})
t.Run("FLOOD_WAIT", func(t *testing.T) {
require.Equal(t, "rpc error code 420: FLOOD_WAIT", tgerr.New(420, "FLOOD_WAIT").Error())
})
}
func TestErrorParse(t *testing.T) {
t.Run("FLOOD_WAIT", func(t *testing.T) {
require.Equal(t, &tgerr.Error{
Code: 420,
Message: "FLOOD_WAIT_359",
Type: "FLOOD_WAIT",
Argument: 359,
}, tgerr.New(420, "FLOOD_WAIT_359"))
})
t.Run("FLOOD_WAIT_0", func(t *testing.T) {
require.Equal(t, &tgerr.Error{
Code: 420,
Message: "FLOOD_WAIT_0",
Type: "FLOOD_WAIT",
Argument: 0,
}, tgerr.New(420, "FLOOD_WAIT_0"))
})
t.Run("Middle", func(t *testing.T) {
require.Equal(t, &tgerr.Error{
Code: 169,
Message: "GO_1337_METERS_AWAY",
Type: "GO_METERS_AWAY",
Argument: 1337,
}, tgerr.New(169, "GO_1337_METERS_AWAY"))
})
}
func TestHelpers(t *testing.T) {
err := func() error {
return tgerr.New(169, "GO_1337_METERS_AWAY")
}()
t.Run("Type", func(t *testing.T) {
require.True(t, tgerr.Is(err, "GO_METERS_AWAY"))
require.True(t, tgerr.Is(err, "FOO", "GO_METERS_AWAY"))
require.False(t, tgerr.Is(err, "NOPE"))
t.Run("AsType", func(t *testing.T) {
{
rpcErr, ok := tgerr.AsType(err, "NOPE")
require.False(t, ok)
require.Nil(t, rpcErr)
}
{
rpcErr, ok := tgerr.AsType(err, "GO_METERS_AWAY")
require.True(t, ok)
require.NotNil(t, rpcErr)
}
})
})
t.Run("Code", func(t *testing.T) {
require.True(t, tgerr.IsCode(err, 169))
require.True(t, tgerr.IsCode(err, 1, 169))
require.False(t, tgerr.IsCode(err, 168))
})
t.Run("Generated", func(t *testing.T) {
// Ensure that code generation works for errors.
err := func() error {
rpcErr := &tgerr.Error{
Type: tg.ErrAccessTokenExpired,
}
return errors.Wrap(rpcErr, "perform operation")
}()
require.True(t, tgerr.Is(err, tg.ErrAccessTokenExpired))
require.True(t, tg.IsAccessTokenExpired(err))
})
t.Run("ErrorType", func(t *testing.T) {
tests := []struct {
name string
value error
}{
{"Nil", nil},
{"WrongType", testutil.TestError()},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := require.New(t)
e := tt.value
_, ok := tgerr.As(e)
a.False(ok)
_, ok = tgerr.AsType(e, "")
a.False(ok)
_, ok = tgerr.AsFloodWait(e)
a.False(ok)
a.False(tgerr.Is(e, ""))
a.False(tgerr.IsCode(e, 0))
})
}
})
}
+77
View File
@@ -0,0 +1,77 @@
package tgerr
import (
"context"
"time"
"go.mau.fi/mautrix-telegram/pkg/gotd/clock"
)
// ErrPremiumFloodWait is error type of "FLOOD_PREMIUM_WAIT" error.
const ErrPremiumFloodWait = "FLOOD_PREMIUM_WAIT"
// ErrFloodWait is error type of "FLOOD_WAIT" error.
const ErrFloodWait = "FLOOD_WAIT"
// FloodWaitErrors is a list of errors that are considered as flood wait.
var FloodWaitErrors = []string{ErrFloodWait, ErrPremiumFloodWait}
// AsFloodWait returns wait duration and true boolean if err is
// the "FLOOD_WAIT" error.
//
// Client should wait for that duration before issuing new requests with
// same method.
func AsFloodWait(err error) (d time.Duration, ok bool) {
for _, e := range FloodWaitErrors {
if rpcErr, ok := AsType(err, e); ok {
return time.Second * time.Duration(rpcErr.Argument), true
}
}
return 0, false
}
type floodWaitOptions struct {
clock clock.Clock
}
// FloodWaitOption configures flood wait.
type FloodWaitOption interface {
apply(o *floodWaitOptions)
}
type floodWaitOptionFunc func(o *floodWaitOptions)
func (f floodWaitOptionFunc) apply(o *floodWaitOptions) {
f(o)
}
// FloodWaitWithClock sets time source for flood wait.
func FloodWaitWithClock(c clock.Clock) FloodWaitOption {
return floodWaitOptionFunc(func(o *floodWaitOptions) {
o.clock = c
})
}
// FloodWait sleeps required duration and returns true if err is FLOOD_WAIT
// or false and context or original error otherwise.
func FloodWait(ctx context.Context, err error, opts ...FloodWaitOption) (bool, error) {
opt := &floodWaitOptions{
clock: clock.System,
}
for _, o := range opts {
o.apply(opt)
}
if d, ok := AsFloodWait(err); ok {
timer := opt.clock.Timer(d + 1*time.Second)
defer clock.StopTimer(timer)
select {
case <-timer.C():
return true, err
case <-ctx.Done():
return false, ctx.Err()
}
}
return false, err
}
+57
View File
@@ -0,0 +1,57 @@
package tgerr
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/gotd/neo"
)
func TestFloodWait(t *testing.T) {
floodWaitErr := New(400, ErrFloodWait)
floodWaitErr.Argument = 3600
ctx := context.Background()
t.Run("WrongType", func(t *testing.T) {
a := assert.New(t)
e := New(0, "wrong")
ok, err := FloodWait(ctx, e)
a.False(ok)
a.ErrorIs(err, e)
})
t.Run("Wait", func(t *testing.T) {
a := assert.New(t)
c := neo.NewTime(time.Now())
e := floodWaitErr
done := make(chan struct{})
observer := c.Observe()
go func() {
ok, err := FloodWait(ctx, e, FloodWaitWithClock(c))
a.True(ok)
a.ErrorIs(err, e)
close(done)
}()
<-observer
c.Travel(3601 * time.Second)
<-done
})
t.Run("ContextDone", func(t *testing.T) {
a := assert.New(t)
c := neo.NewTime(time.Now())
e := floodWaitErr
canceledCtx, cancel := context.WithCancel(ctx)
cancel()
ok, err := FloodWait(canceledCtx, e, FloodWaitWithClock(c))
a.False(ok)
a.ErrorIs(err, context.Canceled)
})
}