From dbe87fafa8fb3c38d6cb22ac335cc76b70b607a6 Mon Sep 17 00:00:00 2001 From: bodqhrohro Date: Fri, 29 Nov 2019 02:51:41 +0200 Subject: [PATCH] Handle updates of user status --- telegram/client.go | 35 ++++----- telegram/connect.go | 11 ++- telegram/handlers.go | 38 +++++++++ telegram/utils.go | 170 ++++++++++++++++++++++++++++++++++++++++ xmpp/component.go | 9 +-- xmpp/gateway/gateway.go | 43 ++++++---- 6 files changed, 264 insertions(+), 42 deletions(-) create mode 100644 telegram/handlers.go create mode 100644 telegram/utils.go diff --git a/telegram/client.go b/telegram/client.go index 6a738af..e8643ce 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -1,10 +1,10 @@ package telegram import ( - "fmt" "github.com/pkg/errors" "path/filepath" "strconv" + "sync" "dev.narayana.im/narayana/telegabber/config" "dev.narayana.im/narayana/telegabber/persistence" @@ -23,6 +23,16 @@ var logConstants = map[string]int32{ ":all": 1023, } +type cacheStruct struct { + chats map[int64]*client.Chat + users map[int32]*client.User +} + +var cache = cacheStruct{ + chats: map[int64]*client.Chat{}, + users: map[int32]*client.User{}, +} + func stringToLogConstant(c string) int32 { level, ok := logConstants[c] if !ok { @@ -44,10 +54,14 @@ type Client struct { jid string Session *persistence.Session - ready chan bool + locks clientLocks online bool } +type clientLocks struct { + authorizationReady sync.WaitGroup +} + // NewClient instantiates a Telegram App func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component, session *persistence.Session) (*Client, error) { logVerbosity := client.WithLogVerbosity(&client.SetLogVerbosityLevelRequest{ @@ -88,21 +102,6 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component jid: jid, Session: session, logVerbosity: logVerbosity, - ready: make(chan bool), + locks: clientLocks{}, }, nil } - -func updateHandler(tdlibClient *client.Client) { - listener := tdlibClient.GetListener() - defer listener.Close() - - for update := range listener.Updates { - if update.GetClass() == client.ClassUpdate { - fmt.Printf("%#v\n", update) - } - } -} - -func (c *Client) ProcessOutgoingMessage(chatID int, text string, messageID int) { - // TODO -} diff --git a/telegram/connect.go b/telegram/connect.go index 4fe262c..f1b3dc5 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -100,6 +100,8 @@ func (c *Client) Connect() error { Password: make(chan string, 1), } + c.locks.authorizationReady.Add(1) + go c.interactor() c.authorizer.TdlibParameters <- c.parameters @@ -111,9 +113,9 @@ func (c *Client) Connect() error { c.client = tdlibClient c.online = true - c.ready <- true + c.locks.authorizationReady.Done() - go updateHandler(c.client) + go c.updateHandler() return nil } @@ -141,6 +143,7 @@ func (c *Client) interactor() { stateType := state.AuthorizationStateType() log.Infof("Telegram authorization state: %#v", stateType) + log.Debugf("%#v", state) switch stateType { case client.TypeAuthorizationStateWaitPhoneNumber: @@ -159,7 +162,7 @@ func (c *Client) interactor() { case client.TypeAuthorizationStateReady: var err error - <-c.ready + c.locks.authorizationReady.Wait() log.Warn("Authorization successful!") @@ -178,7 +181,7 @@ func (c *Client) interactor() { log.Error("Could not retrieve chats") } - gateway.SendPresence(c.xmpp, nil, c.jid, gateway.SPStatus("Logged in "+c.Session.Login)) + gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in "+c.Session.Login)) return } diff --git a/telegram/handlers.go b/telegram/handlers.go new file mode 100644 index 0000000..1195831 --- /dev/null +++ b/telegram/handlers.go @@ -0,0 +1,38 @@ +package telegram + +import ( + log "github.com/sirupsen/logrus" + "github.com/zelenin/go-tdlib/client" +) + +func uhOh() { + log.Fatal("Update type mismatch") +} + +func (c *Client) updateHandler() { + listener := c.client.GetListener() + defer listener.Close() + + for update := range listener.Updates { + if update.GetClass() == client.ClassUpdate { + switch update.GetType() { + case client.TypeUpdateUser: + typedUpdate, ok := update.(*client.UpdateUser) + if !ok { + uhOh() + } + c.updateUser(typedUpdate) + default: + // log only handled types + continue + } + + log.Debugf("%#v", update) + } + } +} + +func (c *Client) updateUser(update *client.UpdateUser) { + cache.users[update.User.Id] = update.User + c.processStatusUpdate(update.User.Id, &update.User.Status) +} diff --git a/telegram/utils.go b/telegram/utils.go new file mode 100644 index 0000000..7fb78d9 --- /dev/null +++ b/telegram/utils.go @@ -0,0 +1,170 @@ +package telegram + +import ( + "crypto/sha1" + "github.com/pkg/errors" + "io" + "os" + "strconv" + "time" + + "dev.narayana.im/narayana/telegabber/xmpp/gateway" + + log "github.com/sirupsen/logrus" + "github.com/soheilhy/args" + "github.com/zelenin/go-tdlib/client" +) + +var errOffline = errors.New("TDlib instance is offline") + +// GetContactByUsername resolves username to user id retrieves user and chat information +func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { + if !c.online { + return nil, nil, errOffline + } + + chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ + Username: username, + }) + + if err != nil { + return nil, nil, err + } + + return c.GetContactByID(int32(chat.Id), chat) +} + +// GetContactByID gets user and chat information from cache (or tries to retrieve it, if missing) +func (c *Client) GetContactByID(id int32, chat *client.Chat) (*client.Chat, *client.User, error) { + if !c.online { + return nil, nil, errOffline + } + + var user *client.User + var cacheChat *client.Chat + var ok bool + var err error + + user, ok = cache.users[id] + if !ok && id > 0 { + user, err = c.client.GetUser(&client.GetUserRequest{ + UserId: id, + }) + if err != nil { + return nil, nil, err + } + + cache.users[id] = user + } + + chatID := int64(id) + cacheChat, ok = cache.chats[chatID] + if !ok { + if chat == nil { + cacheChat, err = c.client.GetChat(&client.GetChatRequest{ + ChatId: chatID, + }) + if err != nil { + return nil, nil, err + } + + cache.chats[chatID] = cacheChat + } else { + cache.chats[chatID] = chat + } + } + if chat == nil { + chat = cacheChat + } + + return chat, user, nil +} + +func (c *Client) processStatusUpdate(chatID int32, status *client.UserStatus, args ...args.V) error { + if !c.online { + return nil + } + + log.WithFields(log.Fields{ + "chat_id": chatID, + }).Info("Status update for") + + chat, user, err := c.GetContactByID(chatID, nil) + if err != nil { + return err + } + + var photo string + if chat != nil && chat.Photo != nil { + path := chat.Photo.Small.Local.Path + file, err := os.Open(path) + if err == nil { + defer file.Close() + + hash := sha1.New() + _, err = io.Copy(hash, file) + if err == nil { + photo = string(hash.Sum(nil)) + } else { + log.Errorf("Error calculating hash: %v", path) + } + } else if path != "" { + log.Errorf("Photo does not exist: %v", path) + } + } + + if status == nil && user != nil { + status = &user.Status + } + + var show, textStatus string + if status == nil { + show = "chat" + if chat.Title != "" { + textStatus = chat.Title + } + } else { + switch (*status).UserStatusType() { + case client.TypeUserStatusOnline: + textStatus = "Online" + case client.TypeUserStatusRecently: + show, textStatus = "dnd", "Last seen recently" + case client.TypeUserStatusLastWeek: + show, textStatus = "unavailable", "Last seen last week" + case client.TypeUserStatusLastMonth: + show, textStatus = "unavailable", "Last seen last month" + case client.TypeUserStatusEmpty: + show, textStatus = "unavailable", "Last seen a long time ago" + case client.TypeUserStatusOffline: + offlineStatus, ok := (*status).(*client.UserStatusOffline) + if !ok { + log.Fatal("Status type changed before conversion!") + } + // this will stop working in 2038 O\ + elapsed := time.Now().Unix() - int64(offlineStatus.WasOnline) + if elapsed < 3600 { + show = "away" + } else { + show = "xa" + } + // TODO: timezone + textStatus = time.Unix(int64(offlineStatus.WasOnline), 0).Format("Last seen at 15:03 02/01/2006") + } + } + + gateway.SendPresence( + c.xmpp, + c.jid, + gateway.SPFrom(strconv.Itoa(int(chatID))), + gateway.SPShow(show), + gateway.SPStatus(textStatus), + gateway.SPPhoto(photo), + gateway.SPImmed(gateway.SPImmed.Get(args)), + ) + + return nil +} + +func (c *Client) ProcessOutgoingMessage(chatID int, text string, messageID int) { + // TODO +} diff --git a/xmpp/component.go b/xmpp/component.go index c0b4a23..57d9c7b 100644 --- a/xmpp/component.go +++ b/xmpp/component.go @@ -17,7 +17,6 @@ const pollingInterval time.Duration = 1e7 var tgConf config.TelegramConfig var sessions map[string]*telegram.Client -var queue gateway.Queue var db persistence.SessionsYamlDB // NewComponent starts a new component and wraps it in @@ -30,8 +29,6 @@ func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig) (*xmpp.Strea return nil, nil, err } - queue = make(gateway.Queue) - tgConf = tc options := xmpp.ComponentOptions{ @@ -69,7 +66,7 @@ func heartbeat(component *xmpp.Component) { for jid := range sessions { for { - err = gateway.SendPresence(component, queue, jid, probeType) + err = gateway.SendPresence(component, jid, probeType) if err == nil { break } @@ -80,12 +77,12 @@ func heartbeat(component *xmpp.Component) { log.Info("Starting heartbeat queue") for { - for key, presence := range queue { + for key, presence := range gateway.Queue { err = component.Send(presence) if err != nil { gateway.LogBadPresence(err, presence) } else { - delete(queue, key) + delete(gateway.Queue, key) } } time.Sleep(60e9) diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 11f68c1..be1f6a3 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -13,7 +13,7 @@ import ( ) // Queue stores presences to send later -type Queue map[string]*stanza.Presence +var Queue = make(map[string]*stanza.Presence) // Jid stores the component's JID object var Jid *xmpp.Jid @@ -101,32 +101,47 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { }} if SPType.IsSet(args) { - presence.Attrs.Type = stanza.StanzaType(SPType.Get(args)) + t := SPType.Get(args) + if t != "" { + presence.Attrs.Type = stanza.StanzaType(t) + } } if SPShow.IsSet(args) { - presence.Show = stanza.PresenceShow(SPShow.Get(args)) + show := SPShow.Get(args) + if show != "" { + presence.Show = stanza.PresenceShow(show) + } } if SPStatus.IsSet(args) { - presence.Status = SPStatus.Get(args) + status := SPStatus.Get(args) + if status != "" { + presence.Status = status + } } if SPNickname.IsSet(args) { - presence.Extensions = append(presence.Extensions, extensions.PresenceNickExtension{ - Text: SPNickname.Get(args), - }) + nickname := SPNickname.Get(args) + if nickname != "" { + presence.Extensions = append(presence.Extensions, extensions.PresenceNickExtension{ + Text: nickname, + }) + } } if SPPhoto.IsSet(args) { - presence.Extensions = append(presence.Extensions, extensions.PresenceXVCardUpdateExtension{ - Photo: extensions.PresenceXVCardUpdatePhoto{ - Text: SPPhoto.Get(args), - }, - }) + photo := SPPhoto.Get(args) + if photo != "" { + presence.Extensions = append(presence.Extensions, extensions.PresenceXVCardUpdateExtension{ + Photo: extensions.PresenceXVCardUpdatePhoto{ + Text: photo, + }, + }) + } } return presence } // SendPresence creates and sends a presence stanza -func SendPresence(component *xmpp.Component, queue Queue, to string, args ...args.V) error { +func SendPresence(component *xmpp.Component, to string, args ...args.V) error { var logFrom string bareJid := Jid.Bare() if SPFrom.IsSet(args) { @@ -161,7 +176,7 @@ func SendPresence(component *xmpp.Component, queue Queue, to string, args ...arg return err } } else { - queue[presence.From+presence.To] = &presence + Queue[presence.From+presence.To] = &presence } return nil