diff --git a/telegram/cache/cache.go b/telegram/cache/cache.go index 3d9608d..c4a59bf 100644 --- a/telegram/cache/cache.go +++ b/telegram/cache/cache.go @@ -19,9 +19,11 @@ type Cache struct { chats map[int64]*client.Chat users map[int64]*client.User statuses map[int64]*Status + capsVers map[int64]string chatsLock sync.Mutex usersLock sync.Mutex statusesLock sync.Mutex + capsVersLock sync.Mutex } // NewCache initializes a cache @@ -106,6 +108,15 @@ func (cache *Cache) GetStatus(id int64) (*Status, bool) { return status, ok } +// GetCapsVer retrieves capabilities verification string by id if it's present in the cache +func (cache *Cache) GetCapsVer(id int64) (string, bool) { + cache.capsVersLock.Lock() + defer cache.capsVersLock.Unlock() + + ver, ok := cache.capsVers[id] + return ver, ok +} + // SetChat stores a chat in the cache func (cache *Cache) SetChat(id int64, chat *client.Chat) { cache.chatsLock.Lock() @@ -133,3 +144,11 @@ func (cache *Cache) SetStatus(id int64, show string, status string) { Description: status, } } + +// SetCapsVer stores a capabilities verification string in the cache +func (cache *Cache) SetCapsVer(id int64, ver string) { + cache.capsVersLock.Lock() + defer cache.capsVersLock.Unlock() + + cache.capsVers[id] = ver +} diff --git a/telegram/utils.go b/telegram/utils.go index 9a247c4..7fa13d3 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -253,6 +253,20 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o newArgs = append(newArgs, gateway.SPType(presenceType)) } + ver, ok := c.cache.GetCapsVer(chatID) + if !ok { + if c.isCallable(chat, user) { + ver, err = gateway.GetCapsVer([]gateway.CapsType{gateway.CapsAudio}}) + if err != nil { + log.Errorf("", err.Error()) + } + } + c.cache.SetCapsVer(ver) + } + if ver != "" { + newArgs = append(newArgs, gateway.SPCaps(ver)) + } + return gateway.SendPresence( c.xmpp, c.jid, @@ -1248,3 +1262,23 @@ func (c *Client) prepareDiskSpace(size uint64) { } } } + +func (c *Client) isCallable(chat *client.Chat, user, *client.User) bool { + if chat == nil || user == nil { + return false + } + chatType := chat.Type.ChatTypeType() + if chatType == client.TypeChatTypePrivate { + privateType, _ := chat.Type.(*client.ChatTypePrivate) + fullInfo, err := c.client.GetUserFullInfo(&client.GetUserFullInfoRequest{ + UserId: privateType.UserId, + }) + if err == nil { + return fullInfo.CanBeCalled && (user.Username != "" || user.PhoneNumber != "") + } else { + log.Warnf("Coudln't retrieve private chat info: %v", err.Error()) + } + } + + return false +} diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 534ee7e..9cd2102 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -1,8 +1,13 @@ package gateway import ( + "bytes" + "encoding/base64" "encoding/xml" "github.com/pkg/errors" + "fmt" + "io" + "sort" "strings" "sync" @@ -37,6 +42,19 @@ var DirtySessions = false // MessageOutgoingPermission allows to fake outgoing messages by foreign JIDs var MessageOutgoingPermission = false +// CapsType is a capability category +type CapsType int +const ( + CapsAudio CapsType = iota +) + +// ContactType is a disco JID category +type ContactType int +const ( + ContactTransport CapsType = iota + ContactPM +) + // SendMessage creates and sends a message stanza func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isOutgoing bool) { sendMessageWrapper(to, from, body, id, component, reply, "", isOutgoing) @@ -225,6 +243,9 @@ var SPResource = args.NewString() // SPImmed skips queueing var SPImmed = args.NewBool(args.Default(true)) +// SPCaps is a XEP-0115 verification string +var SPCaps = args.NewString() + func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { var presenceFrom string if SPFrom.IsSet(args) { @@ -280,6 +301,16 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { }) } } + if SPCaps.IsSet(args) { + ver := SPCaps.Get(args) + if ver != "" { + presence.Extensions = append(presence.Extensions, extensions.CapsExtension{ + Hash: "sha-1", + Node: "https://dev.narayana.im/narayana/telegabber/", + Ver: ver, + }) + } + } return presence } @@ -356,3 +387,88 @@ func SplitJID(from string) (string, string, bool) { } return fromJid.Bare(), fromJid.Resource, true } + +func getDiscoFeatures(caps []CapsType) []string { + features := []string{ + "http://jabber.org/protocol/caps", + "http://jabber.org/protocol/disco#info", + } + for typ := range features { + switch typ { + case CapsAudio: + features = append( + features, + "urn:xmpp:jingle-message:0", + "urn:xmpp:jingle:1", + "urn:xmpp:jingle:apps:dtls:0", + "urn:xmpp:jingle:apps:rtp:1", + "urn:xmpp:jingle:apps:rtp:audio", + "urn:xmpp:jingle:transports:ice-udp:1", + ) + } + } + return features +} + +// GetDiscoInfo generates a disco info IQ query response +func GetDiscoInfo(typ ContactType, features []string) *stanza.DiscoInfo { + disco := stanza.DiscoInfo{} + if typ == ContactPM { + disco.AddIdentity("", "account", "registered") + } else { + disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + } + disco.AddFeatures(features...) + return &disco +} + + +// GetCapsVer hashes a capabilities set into a verification string +func GetCapsVer(caps []CapsType) (string, error) { + features := getDiscoFeatures(caps) + disco := GetDiscoInfo(features) + discoToCapsHash(disco) + buf := new(bytes.Buffer) + binval := base64.NewEncoder(base64.StdEncoding, buf) + _, err = io.Copy(binval, file) + binval.Close() + if err != nil { + return "", errors.Wrap(err, "Error calculating caps base64") + } + return buf.String(), nil +} + +func iOctetComparator(a, b string) bool { + return a < b +} + +func discoToCaps(disco *stanza.DiscoInfo) string { + var s strings.Builder + var identities, vars, capsForms []string + + for _, identity := range disco.Identity { + identities = append(identities, fmt.Sprintf( + "%s/%s//%s", + identity.Category, + identity.Type, + identity.Name, + )) + } + sort.Slice(identities, iOctetComparator) + for _, identity := range identities { + s.WriteString(identity) + s.WriteString(">") + } + + for _, feature := range disco.Features { + vars = append(vars, feature.Var) + } + sort.Slice(vars, iOctetComparator) + for _, var := range vars { + s.WriteString(var) + s.WriteString(">") + } + + for disco + s.WriteString( +} diff --git a/xmpp/gateway/gateway_test.go b/xmpp/gateway/gateway_test.go index 6191844..b75db4d 100644 --- a/xmpp/gateway/gateway_test.go +++ b/xmpp/gateway/gateway_test.go @@ -52,3 +52,8 @@ func TestPresencePhoto(t *testing.T) { presence := newPresence("from@test", "to@test", SPPhoto("01b87fcd030b72895ff8e88db57ec525450f000d")) testPresence(t, presence, "01b87fcd030b72895ff8e88db57ec525450f000d") } + +func TestPresenceCaps(t *testing.T) { + caps := newPresence("from@test", "to@test", SPCaps("QgayPKawpkPSDYmwT/WM94uAlu0=")) + testPresence(t, presence, "") +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index db2b6ea..4e1aea4 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -379,6 +379,11 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { } func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { + iqDisco, ok := iq.Payload.(*stanza.DiscoInfo) + if !ok { + log.Error("Not a disco info request") + return + } answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, @@ -391,13 +396,15 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { return } - disco := answer.DiscoInfo() _, ok := toToID(iq.To) + typ gateway.ContactType if ok { - disco.AddIdentity("", "account", "registered") + typ = gateway.ContactPM } else { - disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + typ = gateway.ContactTransport } + disco := gateway.GetDiscoInfo(typ, []string{}) + disco.Node = iqDisco.Node answer.Payload = disco log.Debugf("%#v", answer)