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.
165 lines
3.9 KiB
165 lines
3.9 KiB
package telegram |
|
|
|
import ( |
|
"context" |
|
"time" |
|
|
|
"github.com/cenkalti/backoff/v4" |
|
"github.com/go-faster/errors" |
|
"go.uber.org/multierr" |
|
"go.uber.org/zap" |
|
|
|
"github.com/gotd/td/internal/exchange" |
|
"github.com/gotd/td/internal/tdsync" |
|
"github.com/gotd/td/telegram/auth" |
|
) |
|
|
|
func (c *Client) runUntilRestart(ctx context.Context) error { |
|
g := tdsync.NewCancellableGroup(ctx) |
|
g.Go(c.conn.Run) |
|
|
|
// If we don't need updates, so there is no reason to subscribe for it. |
|
if !c.noUpdatesMode { |
|
g.Go(func(ctx context.Context) error { |
|
// Call method which requires authorization, to subscribe for updates. |
|
// See https://core.telegram.org/api/updates#subscribing-to-updates. |
|
self, err := c.Self(ctx) |
|
if err != nil { |
|
// Ignore unauthorized errors. |
|
if !auth.IsUnauthorized(err) { |
|
c.log.Warn("Got error on self", zap.Error(err)) |
|
} |
|
return nil |
|
} |
|
|
|
c.log.Info("Got self", zap.String("username", self.Username)) |
|
return nil |
|
}) |
|
} |
|
|
|
g.Go(func(ctx context.Context) error { |
|
select { |
|
case <-ctx.Done(): |
|
return ctx.Err() |
|
case <-c.restart: |
|
c.log.Debug("Restart triggered") |
|
// Should call cancel() to cancel group. |
|
g.Cancel() |
|
|
|
return nil |
|
} |
|
}) |
|
|
|
return g.Wait() |
|
} |
|
|
|
func (c *Client) isPermanentError(err error) bool { |
|
return errors.Is(err, exchange.ErrKeyFingerprintNotFound) |
|
} |
|
|
|
func (c *Client) reconnectUntilClosed(ctx context.Context) error { |
|
// Note that we currently have no timeout on connection, so this is |
|
// potentially eternal. |
|
b := tdsync.SyncBackoff(backoff.WithContext(c.connBackoff(), ctx)) |
|
|
|
return backoff.RetryNotify(func() error { |
|
if err := c.runUntilRestart(ctx); err != nil { |
|
if c.isPermanentError(err) { |
|
return backoff.Permanent(err) |
|
} |
|
return err |
|
} |
|
|
|
return nil |
|
}, b, func(err error, timeout time.Duration) { |
|
c.log.Info("Restarting connection", zap.Error(err), zap.Duration("backoff", timeout)) |
|
|
|
c.connMux.Lock() |
|
c.conn = c.createPrimaryConn(nil) |
|
c.connMux.Unlock() |
|
}) |
|
} |
|
|
|
func (c *Client) onReady() { |
|
c.log.Debug("Ready") |
|
c.ready.Signal() |
|
} |
|
|
|
func (c *Client) resetReady() { |
|
c.ready.Reset() |
|
} |
|
|
|
// Run starts client session and blocks until connection close. |
|
// The f callback is called on successful session initialization and Run |
|
// will return on f() result. |
|
// |
|
// Context of callback will be canceled if fatal error is detected. |
|
// The ctx is used for background operations like updates handling or pools. |
|
// |
|
// See `examples/bg-run` and `contrib/gb` package for classic approach without |
|
// explicit callback, with Connect and defer close(). |
|
func (c *Client) Run(ctx context.Context, f func(ctx context.Context) error) (err error) { |
|
if c.ctx != nil { |
|
select { |
|
case <-c.ctx.Done(): |
|
return errors.Wrap(c.ctx.Err(), "client already closed") |
|
default: |
|
} |
|
} |
|
|
|
// Setting up client context for background operations like updates |
|
// handling or pool creation. |
|
c.ctx, c.cancel = context.WithCancel(ctx) |
|
|
|
c.log.Info("Starting") |
|
defer c.log.Info("Closed") |
|
// Cancel client on exit. |
|
defer c.cancel() |
|
defer func() { |
|
c.subConnsMux.Lock() |
|
defer c.subConnsMux.Unlock() |
|
|
|
for _, conn := range c.subConns { |
|
if closeErr := conn.Close(); !errors.Is(closeErr, context.Canceled) { |
|
multierr.AppendInto(&err, closeErr) |
|
} |
|
} |
|
}() |
|
|
|
c.resetReady() |
|
if err := c.restoreConnection(ctx); err != nil { |
|
return err |
|
} |
|
|
|
g := tdsync.NewCancellableGroup(ctx) |
|
g.Go(c.reconnectUntilClosed) |
|
g.Go(func(ctx context.Context) error { |
|
select { |
|
case <-ctx.Done(): |
|
c.cancel() |
|
return ctx.Err() |
|
case <-c.ctx.Done(): |
|
return c.ctx.Err() |
|
} |
|
}) |
|
g.Go(func(ctx context.Context) error { |
|
select { |
|
case <-ctx.Done(): |
|
return ctx.Err() |
|
case <-c.ready.Ready(): |
|
if err := f(ctx); err != nil { |
|
return errors.Wrap(err, "callback") |
|
} |
|
// Should call cancel() to cancel ctx. |
|
// This will terminate c.conn.Run(). |
|
c.log.Debug("Callback returned, stopping") |
|
g.Cancel() |
|
return nil |
|
} |
|
}) |
|
if err := g.Wait(); !errors.Is(err, context.Canceled) { |
|
return err |
|
} |
|
|
|
return nil |
|
}
|
|
|