package telegram import ( "github.com/pkg/errors" "time" "dev.narayana.im/narayana/telegabber/xmpp/gateway" log "github.com/sirupsen/logrus" "github.com/zelenin/go-tdlib/client" ) const chatsLimit int32 = 999 type clientAuthorizer struct { TdlibParameters chan *client.SetTdlibParametersRequest PhoneNumber chan string Code chan string State chan client.AuthorizationState Password chan string FirstName chan string LastName chan string isClosed bool } func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.AuthorizationState) error { if stateHandler.isClosed { return errors.New("Channel is closed") } stateHandler.State <- state switch state.AuthorizationStateType() { case client.TypeAuthorizationStateWaitTdlibParameters: _, err := c.SetTdlibParameters(<-stateHandler.TdlibParameters) return err case client.TypeAuthorizationStateWaitPhoneNumber: _, err := c.SetAuthenticationPhoneNumber(&client.SetAuthenticationPhoneNumberRequest{ PhoneNumber: <-stateHandler.PhoneNumber, Settings: &client.PhoneNumberAuthenticationSettings{ AllowFlashCall: false, IsCurrentPhoneNumber: false, AllowSmsRetrieverApi: false, }, }) return err case client.TypeAuthorizationStateWaitCode: _, err := c.CheckAuthenticationCode(&client.CheckAuthenticationCodeRequest{ Code: <-stateHandler.Code, }) return err case client.TypeAuthorizationStateWaitRegistration: _, err := c.RegisterUser(&client.RegisterUserRequest{ FirstName: <-stateHandler.FirstName, LastName: <-stateHandler.LastName, }) return err case client.TypeAuthorizationStateWaitPassword: _, err := c.CheckAuthenticationPassword(&client.CheckAuthenticationPasswordRequest{ Password: <-stateHandler.Password, }) return err case client.TypeAuthorizationStateReady: return nil case client.TypeAuthorizationStateLoggingOut: return nil case client.TypeAuthorizationStateClosing: return nil case client.TypeAuthorizationStateClosed: return client.ErrNotSupportedAuthorizationState } return client.ErrNotSupportedAuthorizationState } func (stateHandler *clientAuthorizer) Close() { if stateHandler.isClosed { return } stateHandler.isClosed = true close(stateHandler.TdlibParameters) close(stateHandler.PhoneNumber) close(stateHandler.Code) close(stateHandler.State) close(stateHandler.Password) close(stateHandler.FirstName) close(stateHandler.LastName) } // Connect starts TDlib connection func (c *Client) Connect(resource string) error { log.Warn("Attempting to connect to Telegram network...") // avoid conflict if another authorization is pending already c.locks.authorizationReady.Lock() if c.Online() { c.roster(resource) c.locks.authorizationReady.Unlock() return nil } log.Warn("Connecting to Telegram network...") c.locks.authorizerWriteLock.Lock() c.authorizer = &clientAuthorizer{ TdlibParameters: make(chan *client.SetTdlibParametersRequest, 1), PhoneNumber: make(chan string, 1), Code: make(chan string, 1), State: make(chan client.AuthorizationState, 10), Password: make(chan string, 1), FirstName: make(chan string, 1), LastName: make(chan string, 1), } go c.interactor() log.Warn("Interactor launched") c.authorizer.TdlibParameters <- c.parameters c.locks.authorizerWriteLock.Unlock() tdlibClient, err := client.NewClient(c.authorizer, c.options...) if err != nil { c.locks.authorizationReady.Unlock() return errors.Wrap(err, "Couldn't initialize a Telegram client instance") } c.client = tdlibClient // stage 3: if a client is succesfully created, AuthorizationStateReady is already reached log.Warn("Authorization successful!") c.me, err = c.client.GetMe() if err != nil { log.Error("Could not retrieve me info") } else if c.Session.Login == "" { c.Session.Login = c.me.PhoneNumber } go c.updateHandler() c.online = true c.locks.authorizationReady.Unlock() c.addResource(resource) go func() { _, err = c.client.GetChats(&client.GetChatsRequest{ Limit: chatsLimit, }) if err != nil { log.Errorf("Could not retrieve chats: %v", err) } gateway.SubscribeToTransport(c.xmpp, c.jid) c.sendPresence(gateway.SPStatus("Logged in as: " + c.Session.Login)) }() return nil } func (c *Client) TryLogin(resource string, login string) error { wasSessionLoginEmpty := c.Session.Login == "" c.Session.Login = login if wasSessionLoginEmpty && c.authorizer == nil { go func() { err := c.Connect(resource) if err != nil { log.Error(errors.Wrap(err, "TDlib connection failure")) } }() // a quirk for authorizer to become ready. If it's still not, // nothing bad: just re-login again time.Sleep(1e5) } c.locks.authorizerWriteLock.Lock() defer c.locks.authorizerWriteLock.Unlock() if c.authorizer == nil { return errors.New(TelegramNotInitialized) } if c.authorizer.isClosed { return errors.New(TelegramAuthDone) } return nil } func (c *Client) SetPhoneNumber(login string) error { c.locks.authorizerWriteLock.Lock() defer c.locks.authorizerWriteLock.Unlock() if c.authorizer == nil || c.authorizer.isClosed { return errors.New("Authorization not needed") } c.authorizer.PhoneNumber <- login return nil } // Disconnect drops TDlib connection and // returns the flag indicating if disconnecting is permitted func (c *Client) Disconnect(resource string, quit bool) bool { if !quit { c.deleteResource(resource) } // other resources are still active if (len(c.resources) > 0 || c.Session.KeepOnline) && !quit { log.Infof("Resource %v for account %v has disconnected, %v remaining", resource, c.Session.Login, len(c.resources)) log.Debugf("Resources: %#v", c.resources) return false } // already disconnected if !c.Online() { return false } log.Warn("Disconnecting from Telegram network...") // we're offline (unsubscribe if logout) for _, id := range c.cache.ChatsKeys() { args := gateway.SimplePresence(id, "unavailable") c.sendPresence(args...) } c.close() return true } func (c *Client) interactor() { for { c.locks.authorizerReadLock.Lock() if c.authorizer == nil { log.Warn("Authorizer is lost, halting the interactor") c.locks.authorizerReadLock.Unlock() return } state, ok := <-c.authorizer.State if !ok { log.Warn("Interactor is disconnected") c.locks.authorizerReadLock.Unlock() return } stateType := state.AuthorizationStateType() log.Infof("Telegram authorization state: %#v", stateType) log.Debugf("%#v", state) switch stateType { // stage 0: set login case client.TypeAuthorizationStateWaitPhoneNumber: log.Warn("Logging in...") if c.Session.Login != "" { c.authorizer.PhoneNumber <- c.Session.Login } else { gateway.SendServiceMessage(c.jid, "Please, enter your Telegram login via /login 12345", c.xmpp) } // stage 1: wait for auth code case client.TypeAuthorizationStateWaitCode: log.Warn("Waiting for authorization code...") gateway.SendServiceMessage(c.jid, "Please, enter authorization code via /code 12345", c.xmpp) // stage 1b: wait for registration case client.TypeAuthorizationStateWaitRegistration: log.Warn("Waiting for full name...") gateway.SendServiceMessage(c.jid, "This number is not registered yet! Please, enter your name via /setname John Doe", c.xmpp) // stage 2: wait for 2fa case client.TypeAuthorizationStateWaitPassword: log.Warn("Waiting for 2FA password...") gateway.SendServiceMessage(c.jid, "Please, enter 2FA passphrase via /password 12345", c.xmpp) } c.locks.authorizerReadLock.Unlock() } } func (c *Client) forceClose() { c.locks.authorizerReadLock.Lock() c.locks.authorizerWriteLock.Lock() defer c.locks.authorizerReadLock.Unlock() defer c.locks.authorizerWriteLock.Unlock() c.online = false c.authorizer = nil } func (c *Client) close() { c.locks.authorizerWriteLock.Lock() if c.authorizer != nil && !c.authorizer.isClosed { c.authorizer.Close() } c.locks.authorizerWriteLock.Unlock() if c.client != nil { _, err := c.client.Close() if err != nil { log.Errorf("Couldn't close the Telegram instance: %v; %#v", err, c) } } c.forceClose() } func (c *Client) cancelAuth() { c.close() c.Session.Login = "" } // Online checks if the updates listener is alive func (c *Client) Online() bool { return c.online }