Support avatar notifications and retrieval via XEP-0084
This commit is contained in:
parent
85485bb147
commit
421477ad8c
|
@ -22,6 +22,12 @@ type DelayedStatus struct {
|
|||
TimestampExpired int64
|
||||
}
|
||||
|
||||
// HashedAvatar stores a SHA-1 hash and a Telegram file ID
|
||||
type HashedAvatar struct {
|
||||
Hash string
|
||||
File int32
|
||||
}
|
||||
|
||||
// Client stores the metadata for lazily invoked TDlib instance
|
||||
type Client struct {
|
||||
client *client.Client
|
||||
|
@ -51,6 +57,9 @@ type Client struct {
|
|||
XmppClientFeatures map[string]*[]string
|
||||
XmppClientFeaturesLock sync.Mutex
|
||||
|
||||
AvatarHashes map[int64]*HashedAvatar
|
||||
AvatarHashesLock sync.Mutex
|
||||
|
||||
locks clientLocks
|
||||
SendMessageLock sync.Mutex
|
||||
}
|
||||
|
@ -127,6 +136,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component
|
|||
lastMsgIds: make(map[int64]string),
|
||||
msgHashSeed: maphash.MakeSeed(),
|
||||
XmppClientFeatures: make(map[string]*[]string),
|
||||
AvatarHashes: make(map[int64]*HashedAvatar),
|
||||
locks: clientLocks{
|
||||
chatMessageLocks: make(map[int64]*sync.Mutex),
|
||||
},
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -45,6 +47,11 @@ type messageStub struct {
|
|||
Text string
|
||||
}
|
||||
|
||||
const (
|
||||
typeFileDataSha1 byte = iota
|
||||
typeFileDataBase64
|
||||
)
|
||||
|
||||
var errOffline = errors.New("TDlib instance is offline")
|
||||
|
||||
var spaceRegex = regexp.MustCompile(`\s+`)
|
||||
|
@ -211,6 +218,85 @@ func (c *Client) LastSeenStatus(timestamp int64) string {
|
|||
Format("Last seen at 15:04 02/01/2006")
|
||||
}
|
||||
|
||||
func (c *Client) getFileData(tgFile *client.File, typ byte) string {
|
||||
var priority int32
|
||||
if typ == typeFileDataSha1 {
|
||||
priority = 1
|
||||
} else if typ == typeFileDataBase64 {
|
||||
priority = 32
|
||||
}
|
||||
|
||||
file, path, err := c.ForceOpenFile(tgFile, priority)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
|
||||
if typ == typeFileDataSha1 {
|
||||
hash := sha1.New()
|
||||
_, err = io.Copy(hash, file)
|
||||
if err == nil {
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
} else {
|
||||
log.Errorf("Error calculating hash: %v", path)
|
||||
}
|
||||
} else if typ == typeFileDataBase64 {
|
||||
buf := new(bytes.Buffer)
|
||||
binval := base64.NewEncoder(base64.StdEncoding, buf)
|
||||
_, err = io.Copy(binval, file)
|
||||
binval.Close()
|
||||
if err == nil {
|
||||
return buf.String()
|
||||
} else {
|
||||
log.Errorf("Error calculating base64: %v", path)
|
||||
}
|
||||
}
|
||||
} else if path != "" {
|
||||
log.Errorf("Photo does not exist: %v", path)
|
||||
} else {
|
||||
log.Errorf("PHOTO: %#v", err.Error())
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetEmptyAvatarHash puts a dummy value into the cache to avoid attempting to fetch surely missing avatars
|
||||
func (c *Client) SetEmptyAvatarHash(chatId int64) {
|
||||
c.AvatarHashesLock.Lock()
|
||||
c.AvatarHashes[chatId] = &HashedAvatar{
|
||||
Hash: "",
|
||||
File: 0,
|
||||
}
|
||||
c.AvatarHashesLock.Unlock()
|
||||
}
|
||||
|
||||
// GetPhotoSha1AndSize obtains data for PEP
|
||||
func (c *Client) GetPhotoSha1AndSize(photo *client.File, chatId int64) (string, int64) {
|
||||
sha1 := c.GetPhotoSha1(photo, chatId)
|
||||
|
||||
size := photo.Size
|
||||
if size == 0 {
|
||||
size = photo.ExpectedSize
|
||||
}
|
||||
|
||||
return sha1, size
|
||||
}
|
||||
|
||||
// GetPhotoSha1 computes the photo hash
|
||||
func (c *Client) GetPhotoSha1(photo *client.File, chatId int64) string {
|
||||
sha1 := c.getFileData(photo, typeFileDataSha1)
|
||||
c.AvatarHashesLock.Lock()
|
||||
c.AvatarHashes[chatId] = &HashedAvatar{
|
||||
Hash: sha1,
|
||||
File: photo.Id,
|
||||
}
|
||||
c.AvatarHashesLock.Unlock()
|
||||
return sha1
|
||||
}
|
||||
|
||||
// GetPhotoBase64 reads file data as Base64
|
||||
func (c *Client) GetPhotoBase64(photo *client.File) string {
|
||||
return c.getFileData(photo, typeFileDataBase64)
|
||||
}
|
||||
|
||||
// ProcessStatusUpdate sets contact status
|
||||
func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, oldArgs ...args.V) error {
|
||||
if !c.Online() {
|
||||
|
@ -228,20 +314,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
|
|||
|
||||
var photo string
|
||||
if chat != nil && chat.Photo != nil {
|
||||
file, path, err := c.ForceOpenFile(chat.Photo.Small, 1)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
|
||||
hash := sha1.New()
|
||||
_, err = io.Copy(hash, file)
|
||||
if err == nil {
|
||||
photo = fmt.Sprintf("%x", hash.Sum(nil))
|
||||
} else {
|
||||
log.Errorf("Error calculating hash: %v", path)
|
||||
}
|
||||
} else if path != "" {
|
||||
log.Errorf("Photo does not exist: %v", path)
|
||||
}
|
||||
photo = c.GetPhotoSha1(chat.Photo.Small, chatID)
|
||||
}
|
||||
|
||||
var presenceType string
|
||||
|
@ -1042,6 +1115,24 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
|
|||
c.cache.SetChat(chatId, chat)
|
||||
go c.ProcessStatusUpdate(chatId, "", "", gateway.SPImmed(true))
|
||||
text = "<Chat photo has changed>"
|
||||
|
||||
if chat.Photo == nil {
|
||||
c.SetEmptyAvatarHash(chatId)
|
||||
} else {
|
||||
sha1, size := c.GetPhotoSha1AndSize(chat.Photo.Small, chatId)
|
||||
|
||||
for resource := range c.resourcesRange() {
|
||||
features, ok := c.XmppClientFeatures[resource]
|
||||
if ok && features != nil {
|
||||
for _, feature := range *features {
|
||||
if feature == gateway.NodeAvatarMetadataNotify {
|
||||
go gateway.SendPubSubAvatarNotification(c.xmpp, c.jid+"/"+resource, chatId, sha1, size)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
text = c.messageToText(message, false)
|
||||
|
@ -1263,6 +1354,11 @@ func (c *Client) prepareOutgoingMessageContent(text string, file *client.InputFi
|
|||
return content
|
||||
}
|
||||
|
||||
// ChatsKeys proxies the following function from unexported cache
|
||||
func (c *Client) ChatsKeys() []int64 {
|
||||
return c.cache.ChatsKeys()
|
||||
}
|
||||
|
||||
// StatusesRange proxies the following function from unexported cache
|
||||
func (c *Client) StatusesRange() chan *cache.Status {
|
||||
return c.cache.StatusesRange()
|
||||
|
@ -1337,6 +1433,13 @@ func (c *Client) getLastMessages(id int64, query string, from int64, count int32
|
|||
})
|
||||
}
|
||||
|
||||
// GetFile retrieves a file object by id given by TDlib
|
||||
func (c *Client) GetFile(id int32) (*client.File, error) {
|
||||
return c.client.GetFile(&client.GetFileRequest{
|
||||
FileId: id,
|
||||
})
|
||||
}
|
||||
|
||||
// DownloadFile actually obtains a file by id given by TDlib
|
||||
func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*client.File, error) {
|
||||
return c.client.DownloadFile(&client.DownloadFileRequest{
|
||||
|
|
|
@ -37,6 +37,10 @@ type marker struct {
|
|||
}
|
||||
|
||||
const NSNick string = "http://jabber.org/protocol/nick"
|
||||
const NodeVCard4 string = "urn:xmpp:vcard4"
|
||||
const NodeAvatarMetadata string = "urn:xmpp:avatar:metadata"
|
||||
const NodeAvatarMetadataNotify string = NodeAvatarMetadata + "+notify"
|
||||
const NodeAvatarData string = "urn:xmpp:avatar:data"
|
||||
|
||||
// Queue stores presences to send later
|
||||
var Queue = make(map[string]*stanza.Presence)
|
||||
|
@ -435,3 +439,45 @@ func SplitJID(from string) (string, string, bool) {
|
|||
}
|
||||
return fromJid.Bare(), fromJid.Resource, true
|
||||
}
|
||||
|
||||
// SendPubSubAvatarNotification encourages clients to fetch an avatar
|
||||
func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId int64, sha1 string, size int64) {
|
||||
info := stanza.Node{
|
||||
XMLName: xml.Name{Local: "info"},
|
||||
Attrs: []xml.Attr{
|
||||
xml.Attr{Name: xml.Name{Local: "bytes"}, Value: strconv.FormatInt(size, 10)},
|
||||
xml.Attr{Name: xml.Name{Local: "height"}, Value: "160"},
|
||||
xml.Attr{Name: xml.Name{Local: "id"}, Value: sha1},
|
||||
xml.Attr{Name: xml.Name{Local: "type"}, Value: "image/jpeg"},
|
||||
xml.Attr{Name: xml.Name{Local: "width"}, Value: "160"},
|
||||
},
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
"chatId": chatId,
|
||||
}).Debugf("%#v", info)
|
||||
|
||||
event := &stanza.PubSubEvent{
|
||||
EventElement: &stanza.ItemsEvent{
|
||||
Node: NodeAvatarMetadata,
|
||||
Items: []stanza.ItemEvent{
|
||||
stanza.ItemEvent{
|
||||
Id: sha1,
|
||||
Any: &stanza.Node{
|
||||
XMLName: xml.Name{Local: "metadata", Space: NodeAvatarMetadata},
|
||||
Nodes: []stanza.Node{info},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
message := stanza.Message{
|
||||
Attrs: stanza.Attrs{
|
||||
From: strconv.FormatInt(chatId, 10) + "@" + Jid.Bare(),
|
||||
To: jid,
|
||||
},
|
||||
Extensions: []stanza.MsgExtension{event},
|
||||
}
|
||||
|
||||
_ = ResumableSend(component, message)
|
||||
}
|
||||
|
|
176
xmpp/handlers.go
176
xmpp/handlers.go
|
@ -1,12 +1,9 @@
|
|||
package xmpp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -26,7 +23,6 @@ const (
|
|||
TypeVCardTemp byte = iota
|
||||
TypeVCard4
|
||||
)
|
||||
const NodeVCard4 string = "urn:xmpp:vcard4"
|
||||
|
||||
func logPacketType(p stanza.Packet) {
|
||||
log.Warnf("Ignoring packet: %T\n", p)
|
||||
|
@ -48,11 +44,15 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
|
|||
return
|
||||
}
|
||||
pubsub, ok := iq.Payload.(*stanza.PubSubGeneric)
|
||||
if ok {
|
||||
if pubsub.Items != nil && pubsub.Items.Node == NodeVCard4 {
|
||||
if ok && pubsub.Items != nil {
|
||||
if pubsub.Items.Node == gateway.NodeVCard4 {
|
||||
go handleGetVcardIq(s, iq, TypeVCard4)
|
||||
return
|
||||
}
|
||||
if pubsub.Items.Node == gateway.NodeAvatarData {
|
||||
go handleGetAvatarDataIq(s, iq, pubsub)
|
||||
return
|
||||
}
|
||||
}
|
||||
_, ok = iq.Payload.(*stanza.DiscoInfo)
|
||||
if ok {
|
||||
|
@ -78,7 +78,7 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
|
|||
} else if iq.Type == stanza.IQTypeResult {
|
||||
discoInfo, ok := iq.Payload.(*stanza.DiscoInfo)
|
||||
if ok {
|
||||
go handleClientFeatures(iq, discoInfo)
|
||||
go handleClientFeatures(s, iq, discoInfo)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -476,6 +476,113 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) {
|
|||
_ = gateway.ResumableSend(component, &answer)
|
||||
}
|
||||
|
||||
func handleGetAvatarDataIq(s xmpp.Sender, iq *stanza.IQ, pubsub *stanza.PubSubGeneric) {
|
||||
fromJid, err := stanza.NewJid(iq.From)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid from JID %v", iq.From)
|
||||
return
|
||||
}
|
||||
|
||||
chatId, ok := toToID(iq.To)
|
||||
if !ok {
|
||||
log.Errorf("Invalid chat id in To JID %v", iq.To)
|
||||
return
|
||||
}
|
||||
|
||||
session, ok := sessions[fromJid.Bare()]
|
||||
if !ok {
|
||||
log.Errorf("IQ from stranger %v", iq.From)
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
if len(pubsub.Items.List) > 0 {
|
||||
id = pubsub.Items.List[0].Id
|
||||
}
|
||||
log.Infof("Avatar id %v for chat %v", id, iq.To);
|
||||
|
||||
pubsubAnswer := stanza.PubSubGeneric{
|
||||
Items: &stanza.Items{
|
||||
Node: gateway.NodeAvatarData,
|
||||
},
|
||||
}
|
||||
|
||||
answer := stanza.IQ{
|
||||
Attrs: stanza.Attrs{
|
||||
From: iq.To,
|
||||
To: iq.From,
|
||||
Id: iq.Id,
|
||||
Type: "result",
|
||||
},
|
||||
Payload: &pubsubAnswer,
|
||||
}
|
||||
|
||||
component, ok := s.(*xmpp.Component)
|
||||
if !ok {
|
||||
log.Error("Not a component")
|
||||
return
|
||||
}
|
||||
|
||||
defer gateway.ResumableSend(component, &answer)
|
||||
|
||||
hashedAvatar, ok := session.AvatarHashes[chatId]
|
||||
if !ok {
|
||||
log.Info("Could not find avatar in cache, fetching immediately")
|
||||
|
||||
chat, _, err := session.GetContactByID(chatId, nil)
|
||||
if err != nil || chat == nil || chat.Photo == nil {
|
||||
return
|
||||
}
|
||||
|
||||
file := chat.Photo.Small
|
||||
|
||||
sha1 := session.GetPhotoSha1(file, chatId)
|
||||
hashedAvatar = &telegram.HashedAvatar{
|
||||
Hash: sha1,
|
||||
File: file.Id,
|
||||
}
|
||||
|
||||
session.AvatarHashesLock.Lock()
|
||||
session.AvatarHashes[chatId] = hashedAvatar
|
||||
session.AvatarHashesLock.Unlock()
|
||||
}
|
||||
|
||||
if id != "" && hashedAvatar.Hash != id {
|
||||
log.Infof("Cache contains %v hash for chat %v, but %v was requested; aborting", hashedAvatar.Hash, iq.To, id)
|
||||
return
|
||||
}
|
||||
if hashedAvatar.File == 0 {
|
||||
log.Infof("Avatar for chat %v is explicitly missing", iq.To)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := session.GetFile(hashedAvatar.File)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"chatId": chatId,
|
||||
}).Error(errors.Wrap(err, "Cannot get avatar file"))
|
||||
return
|
||||
}
|
||||
|
||||
dataString := session.GetPhotoBase64(file)
|
||||
if dataString == "" {
|
||||
log.Errorf("Error reading avatar file for chat %v", iq.To)
|
||||
return
|
||||
}
|
||||
|
||||
pubsubAnswer.Items.List = append(pubsubAnswer.Items.List, stanza.Item{
|
||||
Id: hashedAvatar.Hash,
|
||||
Any: &stanza.Node{
|
||||
XMLName: xml.Name{Local: "data", Space: gateway.NodeAvatarData},
|
||||
Content: dataString,
|
||||
},
|
||||
})
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"length": len(dataString),
|
||||
}).Debugf("%#v", answer)
|
||||
}
|
||||
|
||||
func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
|
||||
answer, err := stanza.NewIQ(stanza.Attrs{
|
||||
Type: stanza.IQTypeResult,
|
||||
|
@ -716,7 +823,7 @@ func probeClientFeatures(jid string, component *xmpp.Component) {
|
|||
gateway.ResumableSend(component, &probe)
|
||||
}
|
||||
|
||||
func handleClientFeatures(iq *stanza.IQ, discoInfo *stanza.DiscoInfo) {
|
||||
func handleClientFeatures(s xmpp.Sender, iq *stanza.IQ, discoInfo *stanza.DiscoInfo) {
|
||||
fromJid, err := stanza.NewJid(iq.From)
|
||||
if err != nil {
|
||||
log.Error("Invalid from JID!")
|
||||
|
@ -731,8 +838,12 @@ func handleClientFeatures(iq *stanza.IQ, discoInfo *stanza.DiscoInfo) {
|
|||
}
|
||||
|
||||
var features []string
|
||||
var avatarNotify bool
|
||||
for _, feature := range discoInfo.Features {
|
||||
features = append(features, feature.Var)
|
||||
if feature.Var == gateway.NodeAvatarMetadataNotify {
|
||||
avatarNotify = true
|
||||
}
|
||||
}
|
||||
|
||||
session.XmppClientFeaturesLock.Lock()
|
||||
|
@ -740,6 +851,34 @@ func handleClientFeatures(iq *stanza.IQ, discoInfo *stanza.DiscoInfo) {
|
|||
session.XmppClientFeaturesLock.Unlock()
|
||||
|
||||
log.Debugf("Features for %v: %#v", iq.From, features)
|
||||
|
||||
if avatarNotify {
|
||||
go sendPubSubAvatarNotifications(s, iq.From, session)
|
||||
}
|
||||
}
|
||||
|
||||
func sendPubSubAvatarNotifications(s xmpp.Sender, jid string, session *telegram.Client) {
|
||||
component, ok := s.(*xmpp.Component)
|
||||
if !ok {
|
||||
log.Error("Not a component")
|
||||
return
|
||||
}
|
||||
|
||||
for _, chatId := range session.ChatsKeys() {
|
||||
chat, _, err := session.GetContactByID(chatId, nil)
|
||||
if err != nil || chat == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if chat.Photo == nil {
|
||||
session.SetEmptyAvatarHash(chatId)
|
||||
continue
|
||||
}
|
||||
|
||||
sha1, size := session.GetPhotoSha1AndSize(chat.Photo.Small, chat.Id)
|
||||
|
||||
gateway.SendPubSubAvatarNotification(component, jid, chat.Id, sha1, size)
|
||||
}
|
||||
}
|
||||
|
||||
func toToID(to string) (int64, bool) {
|
||||
|
@ -760,24 +899,7 @@ func toToID(to string) (int64, bool) {
|
|||
func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload {
|
||||
var base64Photo string
|
||||
if info.Photo != nil {
|
||||
file, path, err := session.ForceOpenFile(info.Photo, 32)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
binval := base64.NewEncoder(base64.StdEncoding, buf)
|
||||
_, err = io.Copy(binval, file)
|
||||
binval.Close()
|
||||
if err == nil {
|
||||
base64Photo = buf.String()
|
||||
} else {
|
||||
log.Errorf("Error calculating base64: %v", path)
|
||||
}
|
||||
} else if path != "" {
|
||||
log.Errorf("Photo does not exist: %v", path)
|
||||
} else {
|
||||
log.Errorf("PHOTO: %#v", err.Error())
|
||||
}
|
||||
base64Photo = session.GetPhotoBase64(info.Photo)
|
||||
}
|
||||
|
||||
if typ == TypeVCardTemp {
|
||||
|
@ -878,7 +1000,7 @@ func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *tel
|
|||
|
||||
pubsub := &stanza.PubSubGeneric{
|
||||
Items: &stanza.Items{
|
||||
Node: NodeVCard4,
|
||||
Node: gateway.NodeVCard4,
|
||||
List: []stanza.Item{
|
||||
stanza.Item{
|
||||
Id: id,
|
||||
|
|
Loading…
Reference in a new issue