You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
172 lines
4.2 KiB
172 lines
4.2 KiB
package telegram |
|
|
|
import ( |
|
"context" |
|
"io" |
|
"os" |
|
"path/filepath" |
|
"strconv" |
|
"time" |
|
|
|
"github.com/cenkalti/backoff/v4" |
|
"github.com/go-faster/errors" |
|
"go.uber.org/zap" |
|
"golang.org/x/net/proxy" |
|
|
|
"github.com/gotd/td/clock" |
|
"github.com/gotd/td/internal/crypto" |
|
"github.com/gotd/td/session" |
|
"github.com/gotd/td/telegram/auth" |
|
"github.com/gotd/td/telegram/dcs" |
|
"github.com/gotd/td/tgerr" |
|
) |
|
|
|
func sessionDir() (string, error) { |
|
dir, ok := os.LookupEnv("SESSION_DIR") |
|
if ok { |
|
return filepath.Abs(dir) |
|
} |
|
|
|
dir, err := os.UserHomeDir() |
|
if err != nil { |
|
dir = "." |
|
} |
|
|
|
return filepath.Abs(filepath.Join(dir, ".td")) |
|
} |
|
|
|
// OptionsFromEnvironment fills unfilled field in opts parameter |
|
// using environment variables. |
|
// |
|
// Variables: |
|
// SESSION_FILE: path to session file |
|
// SESSION_DIR: path to session directory, if SESSION_FILE is not set |
|
// ALL_PROXY, NO_PROXY: see https://pkg.go.dev/golang.org/x/net/proxy#FromEnvironment |
|
func OptionsFromEnvironment(opts Options) (Options, error) { |
|
// Setting up session storage if not provided. |
|
if opts.SessionStorage == nil { |
|
sessionFile, ok := os.LookupEnv("SESSION_FILE") |
|
if !ok { |
|
dir, err := sessionDir() |
|
if err != nil { |
|
return Options{}, errors.Wrap(err, "SESSION_DIR not set or invalid") |
|
} |
|
sessionFile = filepath.Join(dir, "session.json") |
|
} |
|
|
|
dir, _ := filepath.Split(sessionFile) |
|
if err := os.MkdirAll(dir, 0700); err != nil { |
|
return Options{}, errors.Wrap(err, "session dir creation") |
|
} |
|
|
|
opts.SessionStorage = &session.FileStorage{ |
|
Path: sessionFile, |
|
} |
|
} |
|
|
|
if opts.Resolver == nil { |
|
opts.Resolver = dcs.Plain(dcs.PlainOptions{ |
|
Dial: proxy.Dial, |
|
}) |
|
} |
|
|
|
return opts, nil |
|
} |
|
|
|
// ClientFromEnvironment creates client using OptionsFromEnvironment |
|
// but does not connect to server. |
|
// |
|
// Variables: |
|
// APP_ID: app_id of Telegram app. |
|
// APP_HASH: app_hash of Telegram app. |
|
func ClientFromEnvironment(opts Options) (*Client, error) { |
|
appID, err := strconv.Atoi(os.Getenv("APP_ID")) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "APP_ID not set or invalid") |
|
} |
|
|
|
appHash := os.Getenv("APP_HASH") |
|
if appHash == "" { |
|
return nil, errors.New("no APP_HASH provided") |
|
} |
|
|
|
opts, err = OptionsFromEnvironment(opts) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
return NewClient(appID, appHash, opts), nil |
|
} |
|
|
|
func retry(ctx context.Context, logger *zap.Logger, cb func(ctx context.Context) error) error { |
|
b := backoff.WithContext(backoff.NewExponentialBackOff(), ctx) |
|
|
|
// List of known retryable RPC error types. |
|
retryableErrors := []string{ |
|
"NEED_MEMBER_INVALID", |
|
"AUTH_KEY_UNREGISTERED", |
|
"API_ID_PUBLISHED_FLOOD", |
|
} |
|
|
|
return backoff.Retry(func() error { |
|
if err := cb(ctx); err != nil { |
|
logger.Warn("TestClient run failed", zap.Error(err)) |
|
|
|
if tgerr.Is(err, retryableErrors...) { |
|
return err |
|
} |
|
if timeout, ok := AsFloodWait(err); ok { |
|
timer := clock.System.Timer(timeout + 1*time.Second) |
|
defer clock.StopTimer(timer) |
|
|
|
select { |
|
case <-timer.C(): |
|
return err |
|
case <-ctx.Done(): |
|
return ctx.Err() |
|
} |
|
} |
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { |
|
// Possibly server closed connection. |
|
return err |
|
} |
|
|
|
return backoff.Permanent(err) |
|
} |
|
|
|
return nil |
|
}, b) |
|
} |
|
|
|
// TestClient creates and authenticates user telegram.Client |
|
// using Telegram test server. |
|
func TestClient(ctx context.Context, opts Options, cb func(ctx context.Context, client *Client) error) error { |
|
if opts.DC == 0 { |
|
opts.DC = 2 |
|
} |
|
if opts.DCList.Zero() { |
|
opts.DCList = dcs.Test() |
|
} |
|
|
|
logger := zap.NewNop() |
|
if opts.Logger != nil { |
|
logger = opts.Logger.Named("test") |
|
} |
|
|
|
// Sometimes testing server can return "AUTH_KEY_UNREGISTERED" error. |
|
// It is expected and client implementation is unlikely to cause |
|
// such errors, so just doing retries using backoff. |
|
return retry(ctx, logger, func(retryCtx context.Context) error { |
|
client := NewClient(TestAppID, TestAppHash, opts) |
|
return client.Run(retryCtx, func(runCtx context.Context) error { |
|
if err := client.Auth().IfNecessary(runCtx, auth.NewFlow( |
|
auth.Test(crypto.DefaultRand(), opts.DC), |
|
auth.SendCodeOptions{}, |
|
)); err != nil { |
|
return errors.Wrap(err, "auth flow") |
|
} |
|
|
|
return cb(runCtx, client) |
|
}) |
|
}) |
|
}
|
|
|