Merge branch 'master' into muc

This commit is contained in:
Bohdan Horbeshko 2023-09-16 23:16:09 -04:00
commit 9dbd487dae
22 changed files with 2378 additions and 439 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ telegabber
sessions/ sessions/
session.dat session.dat
session.dat.new session.dat.new
release/

36
Dockerfile Normal file
View file

@ -0,0 +1,36 @@
FROM golang:1.19-bookworm AS base
RUN apt-get update
run apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git
FROM base AS tdlib
ARG TD_COMMIT
ARG MAKEOPTS
RUN git clone https://github.com/tdlib/td /src/
RUN git -C /src/ checkout "${TD_COMMIT}"
RUN mkdir build
WORKDIR /build/
RUN cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/compiled/ /src/
RUN cmake --build . ${MAKEOPTS}
RUN make install
FROM base AS cache
ARG VERSION
COPY --from=tdlib /compiled/ /usr/local/
COPY ./ /src
RUN git -C /src checkout "${VERSION}"
WORKDIR /src
RUN go get
FROM cache AS build
ARG MAKEOPTS
WORKDIR /src
RUN make ${MAKEOPTS}
FROM scratch AS telegabber
COPY --from=build /src/telegabber /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/telegabber"]
FROM scratch AS binaries
COPY --from=telegabber /usr/local/bin/telegabber /

View file

@ -1,10 +1,18 @@
.PHONY: all test .PHONY: all test
COMMIT := $(shell git rev-parse --short HEAD)
TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1"
VERSION := "v2.0.0-dev"
MAKEOPTS := "-j4"
all: all:
go build -o telegabber go build -ldflags "-X main.commit=${COMMIT}" -o telegabber
test: test:
go test -v ./config ./ ./telegram ./xmpp ./xmpp/gateway ./persistence ./telegram/formatter go test -v ./config ./ ./telegram ./xmpp ./xmpp/gateway ./persistence ./telegram/formatter ./badger
lint: lint:
$(GOPATH)/bin/golint ./... $(GOPATH)/bin/golint ./...
build_indocker:
docker build --build-arg "TD_COMMIT=${TD_COMMIT}" --build-arg "VERSION=${VERSION}" --build-arg "MAKEOPTS=${MAKEOPTS}" --output=release --target binaries .

View file

@ -75,6 +75,7 @@ It is good idea to obtain Telegram API ID from [**https://my.telegram.org**](htt
* `--profiling-port=xxxx`: start the pprof server on port `xxxx`. Access is limited to localhost. * `--profiling-port=xxxx`: start the pprof server on port `xxxx`. Access is limited to localhost.
* `--config=/bla/bla/config.yml`: set the config file path (default: `config.yml`). * `--config=/bla/bla/config.yml`: set the config file path (default: `config.yml`).
* `--schema=/bla/bla/schema.json`: set the schema file path (default: `./config_schema.json`). * `--schema=/bla/bla/schema.json`: set the schema file path (default: `./config_schema.json`).
* `--ids=/bla/bla/ids`: set the folder for ids database (default: `ids`).
### How to receive files from Telegram ### ### How to receive files from Telegram ###
@ -142,3 +143,34 @@ server {
``` ```
Finally, update `:upload:` in your config.yml to match `server_name` in nginx config. Finally, update `:upload:` in your config.yml to match `server_name` in nginx config.
### Carbons ###
Telegabber needs special privileges according to XEP-0356 to simulate message carbons from the users (to display messages they have sent earlier or via other clients). Example configuration for Prosody:
```
modules_enabled = {
[...]
"privilege";
}
[...]
Component "telegabber.yourdomain.tld"
component_secret = "yourpassword"
modules_enabled = {"privilege"}
[...]
VirtualHost "yourdomain.tld"
[...]
privileged_entities = {
[...]
["telegabber.yourdomain.tld"] = {
message = "outgoing";
},
}
```

230
badger/ids.go Normal file
View file

@ -0,0 +1,230 @@
package badger
import (
"bytes"
"errors"
"fmt"
"strconv"
badger "github.com/dgraph-io/badger/v4"
log "github.com/sirupsen/logrus"
)
// IdsDB represents a Badger database
type IdsDB struct {
db *badger.DB
}
// IdsDBOpen returns a new DB object
func IdsDBOpen(path string) IdsDB {
bdb, err := badger.Open(badger.DefaultOptions(path))
if err != nil {
log.Errorf("Failed to open ids database: %v, falling back to in-memory database", path)
bdb, err = badger.Open(badger.DefaultOptions("").WithInMemory(true))
if err != nil {
log.Fatalf("Couldn't initialize the ids database")
}
}
return IdsDB{
db: bdb,
}
}
// Set stores an id pair
func (db *IdsDB) Set(tgAccount, xmppAccount string, tgChatId, tgMsgId int64, xmppId string) error {
bPrefix := toKeyPrefix(tgAccount, xmppAccount)
bTgId := toTgByteString(tgChatId, tgMsgId)
bXmppId := toXmppByteString(xmppId)
bTgKey := toByteKey(bPrefix, bTgId, "tg")
bXmppKey := toByteKey(bPrefix, bXmppId, "xmpp")
return db.db.Update(func(txn *badger.Txn) error {
if err := txn.Set(bTgKey, bXmppId); err != nil {
return err
}
return txn.Set(bXmppKey, bTgId)
})
}
func (db *IdsDB) getByteValue(key []byte) ([]byte, error) {
var valCopy []byte
err := db.db.View(func(txn *badger.Txn) error {
item, err := txn.Get(key)
if err != nil {
return err
}
valCopy, err = item.ValueCopy(nil)
return err
})
return valCopy, err
}
// GetByTgIds obtains an XMPP id by Telegram chat/message ids
func (db *IdsDB) GetByTgIds(tgAccount, xmppAccount string, tgChatId, tgMsgId int64) (string, error) {
val, err := db.getByteValue(toByteKey(
toKeyPrefix(tgAccount, xmppAccount),
toTgByteString(tgChatId, tgMsgId),
"tg",
))
if err != nil {
return "", err
}
return string(val), nil
}
// GetByXmppId obtains Telegram chat/message ids by an XMPP id
func (db *IdsDB) GetByXmppId(tgAccount, xmppAccount, xmppId string) (int64, int64, error) {
val, err := db.getByteValue(toByteKey(
toKeyPrefix(tgAccount, xmppAccount),
toXmppByteString(xmppId),
"xmpp",
))
if err != nil {
return 0, 0, err
}
return splitTgByteString(val)
}
func toKeyPrefix(tgAccount, xmppAccount string) []byte {
return []byte(fmt.Sprintf("%v/%v/", tgAccount, xmppAccount))
}
func toByteKey(prefix, suffix []byte, typ string) []byte {
key := make([]byte, 0, len(prefix)+len(suffix)+6)
key = append(key, prefix...)
key = append(key, []byte(typ)...)
key = append(key, []byte("/")...)
key = append(key, suffix...)
return key
}
func toTgByteString(tgChatId, tgMsgId int64) []byte {
return []byte(fmt.Sprintf("%v/%v", tgChatId, tgMsgId))
}
func toXmppByteString(xmppId string) []byte {
return []byte(xmppId)
}
func splitTgByteString(val []byte) (int64, int64, error) {
parts := bytes.Split(val, []byte("/"))
if len(parts) != 2 {
return 0, 0, errors.New("Couldn't parse tg id pair")
}
tgChatId, err := strconv.ParseInt(string(parts[0]), 10, 64)
if err != nil {
return 0, 0, err
}
tgMsgId, err := strconv.ParseInt(string(parts[1]), 10, 64)
return tgChatId, tgMsgId, err
}
// ReplaceIdPair replaces an old entry by XMPP ID with both new XMPP and Tg ID
func (db *IdsDB) ReplaceIdPair(tgAccount, xmppAccount, oldXmppId, newXmppId string, newMsgId int64) error {
// read old pair
chatId, oldMsgId, err := db.GetByXmppId(tgAccount, xmppAccount, oldXmppId)
if err != nil {
return err
}
bPrefix := toKeyPrefix(tgAccount, xmppAccount)
bOldTgId := toTgByteString(chatId, oldMsgId)
bOldXmppId := toXmppByteString(oldXmppId)
bOldTgKey := toByteKey(bPrefix, bOldTgId, "tg")
bOldXmppKey := toByteKey(bPrefix, bOldXmppId, "xmpp")
bTgId := toTgByteString(chatId, newMsgId)
bXmppId := toXmppByteString(newXmppId)
bTgKey := toByteKey(bPrefix, bTgId, "tg")
bXmppKey := toByteKey(bPrefix, bXmppId, "xmpp")
return db.db.Update(func(txn *badger.Txn) error {
// save new pair
if err := txn.Set(bTgKey, bXmppId); err != nil {
return err
}
if err := txn.Set(bXmppKey, bTgId); err != nil {
return err
}
// delete old pair
if err := txn.Delete(bOldTgKey); err != nil {
return err
}
return txn.Delete(bOldXmppKey)
})
}
// ReplaceXmppId replaces an old XMPP ID with new XMPP ID and keeps Tg ID intact
func (db *IdsDB) ReplaceXmppId(tgAccount, xmppAccount, oldXmppId, newXmppId string) error {
// read old Tg IDs
chatId, msgId, err := db.GetByXmppId(tgAccount, xmppAccount, oldXmppId)
if err != nil {
return err
}
bPrefix := toKeyPrefix(tgAccount, xmppAccount)
bOldXmppId := toXmppByteString(oldXmppId)
bOldXmppKey := toByteKey(bPrefix, bOldXmppId, "xmpp")
bTgId := toTgByteString(chatId, msgId)
bXmppId := toXmppByteString(newXmppId)
bTgKey := toByteKey(bPrefix, bTgId, "tg")
bXmppKey := toByteKey(bPrefix, bXmppId, "xmpp")
return db.db.Update(func(txn *badger.Txn) error {
// save new pair
if err := txn.Set(bTgKey, bXmppId); err != nil {
return err
}
if err := txn.Set(bXmppKey, bTgId); err != nil {
return err
}
// delete old xmpp->tg entry
return txn.Delete(bOldXmppKey)
})
}
// ReplaceTgId replaces an old Tg ID with new Tg ID and keeps Tg chat ID and XMPP ID intact
func (db *IdsDB) ReplaceTgId(tgAccount, xmppAccount string, chatId, oldMsgId, newMsgId int64) error {
// read old XMPP ID
xmppId, err := db.GetByTgIds(tgAccount, xmppAccount, chatId, oldMsgId)
if err != nil {
return err
}
bPrefix := toKeyPrefix(tgAccount, xmppAccount)
bOldTgId := toTgByteString(chatId, oldMsgId)
bOldTgKey := toByteKey(bPrefix, bOldTgId, "tg")
bTgId := toTgByteString(chatId, newMsgId)
bXmppId := toXmppByteString(xmppId)
bTgKey := toByteKey(bPrefix, bTgId, "tg")
bXmppKey := toByteKey(bPrefix, bXmppId, "xmpp")
return db.db.Update(func(txn *badger.Txn) error {
// save new pair
if err := txn.Set(bTgKey, bXmppId); err != nil {
return err
}
if err := txn.Set(bXmppKey, bTgId); err != nil {
return err
}
// delete old tg->xmpp entry
return txn.Delete(bOldTgKey)
})
}
// Gc compacts the value log
func (db *IdsDB) Gc() {
db.db.RunValueLogGC(0.7)
}
// Close closes a DB
func (db *IdsDB) Close() {
db.db.Close()
}

72
badger/ids_test.go Normal file
View file

@ -0,0 +1,72 @@
package badger
import (
"reflect"
"testing"
)
func TestToKeyPrefix(t *testing.T) {
if !reflect.DeepEqual(toKeyPrefix("+123456789", "test@example.com"), []byte("+123456789/test@example.com/")) {
t.Error("Wrong prefix")
}
}
func TestToByteKey(t *testing.T) {
if !reflect.DeepEqual(toByteKey([]byte("ababa/galamaga/"), []byte("123"), "ppp"), []byte("ababa/galamaga/ppp/123")) {
t.Error("Wrong key")
}
}
func TestToTgByteString(t *testing.T) {
if !reflect.DeepEqual(toTgByteString(-2345, 6789), []byte("-2345/6789")) {
t.Error("Wrong tg string")
}
}
func TestToXmppByteString(t *testing.T) {
if !reflect.DeepEqual(toXmppByteString("aboba"), []byte("aboba")) {
t.Error("Wrong xmpp string")
}
}
func TestSplitTgByteStringUnparsable(t *testing.T) {
_, _, err := splitTgByteString([]byte("@#U*&$(@#"))
if err == nil {
t.Error("Unparsable should not be parsed")
return
}
if err.Error() != "Couldn't parse tg id pair" {
t.Error("Wrong parse error")
}
}
func TestSplitTgByteManyParts(t *testing.T) {
_, _, err := splitTgByteString([]byte("a/b/c/d"))
if err == nil {
t.Error("Should not parse many parts")
return
}
if err.Error() != "Couldn't parse tg id pair" {
t.Error("Wrong parse error")
}
}
func TestSplitTgByteNonNumeric(t *testing.T) {
_, _, err := splitTgByteString([]byte("0/a"))
if err == nil {
t.Error("Should not parse non-numeric msgid")
}
}
func TestSplitTgByteSuccess(t *testing.T) {
chatId, msgId, err := splitTgByteString([]byte("-198282398/23798478"))
if err != nil {
t.Error("Should be parsed well")
}
if chatId != -198282398 {
t.Error("Wrong chatId")
}
if msgId != 23798478 {
t.Error("Wrong msgId")
}
}

27
go.mod
View file

@ -1,10 +1,10 @@
module dev.narayana.im/narayana/telegabber module dev.narayana.im/narayana/telegabber
go 1.13 go 1.19
require ( require (
github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 github.com/dgraph-io/badger/v4 v4.1.0
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.9.1
github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/santhosh-tekuri/jsonschema v1.2.4
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
github.com/soheilhy/args v0.0.0-20150720134047-6bcf4c78e87e github.com/soheilhy/args v0.0.0-20150720134047-6bcf4c78e87e
@ -13,4 +13,25 @@ require (
gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1 gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1
) )
require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/klauspost/compress v1.12.3 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
go.opencensus.io v0.22.5 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
nhooyr.io/websocket v1.6.5 // indirect
)
replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f
replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615

124
go.sum
View file

@ -1,36 +1,35 @@
dev.narayana.im/narayana/go-xmpp v0.0.0-20211218155535-e55463fc9829 h1:qe81G6+t1V1ySRMa7lSu5CayN5aP5GEiHXL2DYwHzuA= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dev.narayana.im/narayana/go-xmpp v0.0.0-20211218155535-e55463fc9829/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615 h1:RRUZJSro+k8FkazNx7QEYLVoO4wZtchvsd0Y2RBWjeU=
dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU=
dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f h1:6249ajbMjgYz53Oq0IjTvjHXbxTfu29Mj1J/6swRHs4= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f h1:6249ajbMjgYz53Oq0IjTvjHXbxTfu29Mj1J/6swRHs4=
dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f h1:aT50UsPH1dLje9CCAquRRhr7I9ZvL3kQU6WIWTe8PZ0= dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f h1:aT50UsPH1dLje9CCAquRRhr7I9ZvL3kQU6WIWTe8PZ0=
dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 h1:GbV1Lv3lVHsSeKAqPTBem72OCsGjXntW4jfJdXciE+w= github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 h1:GbV1Lv3lVHsSeKAqPTBem72OCsGjXntW4jfJdXciE+w=
github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7/go.mod h1:ZzkRfuaFj8etIYMj/ECtXtgfz72RE6U+dos27b3XIwk= github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7/go.mod h1:ZzkRfuaFj8etIYMj/ECtXtgfz72RE6U+dos27b3XIwk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
github.com/bodqhrohro/go-tdlib v0.1.1 h1:lmHognymABxP3cmHkfAGhGnWaJaZ3htpJ7RSbZacin4= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121200156-e826071d3317 h1:+mv4FwWXl8hTa7PrhekwVzPknH+rHqB60jIPBi2XqI8= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121200156-e826071d3317/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121233100-48d2382034fb h1:y5PnjdAnNVS0q8xuwjm3TxBfLriJmykQdoGiyYZB3s0=
github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121233100-48d2382034fb/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU=
github.com/bodqhrohro/go-tdlib v0.4.4-0.20211229000346-ee6018be8ec0 h1:9ysLk2hG2q0NeNdX6StzS+4fTAG2FeZJYKKegCuB4q4=
github.com/bodqhrohro/go-tdlib v0.4.4-0.20211229000346-ee6018be8ec0/go.mod h1:sOdXFpJ3zn6RHRc8aNVkJYALHpoplwBgMwIbRCYABIg=
github.com/bodqhrohro/go-xmpp v0.1.4-0.20191106203535-f3b463f3b26c h1:LzcQyE+Gs+0kAbpnPAUD68FvUCieKZip44URAmH70PI=
github.com/bodqhrohro/go-xmpp v0.1.4-0.20191106203535-f3b463f3b26c/go.mod h1:fWixaMaFvx8cxXcJVJ5kU9csMeD/JN8on7ybassU8rY=
github.com/bodqhrohro/go-xmpp v0.2.1-0.20191105232737-9abd5be0aa1b h1:9BLd/SNO4JJZLRl1Qb1v9mNivIlHuwHDe2c8hQvBxFA=
github.com/bodqhrohro/go-xmpp v0.2.1-0.20191105232737-9abd5be0aa1b/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
github.com/bodqhrohro/go-xmpp v0.2.1-0.20211205194122-f8c4ecb59d8b h1:rTK55SNCBmssyRgNAweVwVVfuoRstI8RbL+8Ys/RzxE=
github.com/bodqhrohro/go-xmpp v0.2.1-0.20211205194122-f8c4ecb59d8b/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
github.com/bodqhrohro/go-xmpp v0.2.1-0.20211218153313-a8aadd78b65b h1:VDi8z3PzEDhQzazRRuv1fkv662DT3Mm/TY/Lni2Sgrc=
github.com/bodqhrohro/go-xmpp v0.2.1-0.20211218153313-a8aadd78b65b/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v4 v4.1.0 h1:E38jc0f+RATYrycSUf9LMv/t47XAy+3CApyYSq4APOQ=
github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@ -40,25 +39,43 @@ github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/godcong/go-tdlib v0.4.4-0.20211203152853-64d22ab8d4ac h1:5FQGW4yHSkbwm+4i/8ef7FvkIFt4NOM4HexSbvPduRo= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/godcong/go-tdlib v0.4.4-0.20211203152853-64d22ab8d4ac/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -74,8 +91,9 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
@ -89,48 +107,95 @@ github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
github.com/zelenin/go-tdlib v0.1.0 h1:Qq+FGE0/EWdsRB6m26ULDndu2DtW558aFXNzi0Y/FqQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zelenin/go-tdlib v0.1.0/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zelenin/go-tdlib v0.5.2 h1:inEATEM0Pz6/HBI3wTlhd+brDHpmoXGgwdSb8/V6GiA= github.com/zelenin/go-tdlib v0.5.2 h1:inEATEM0Pz6/HBI3wTlhd+brDHpmoXGgwdSb8/V6GiA=
github.com/zelenin/go-tdlib v0.5.2/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= github.com/zelenin/go-tdlib v0.5.2/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU=
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@ -138,12 +203,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gosrc.io/xmpp v0.1.3 h1:VYP1bA35irlQ1ZAJqNhJOz8NSsSTkzQRhREfmuG1H80=
gosrc.io/xmpp v0.1.3/go.mod h1:fWixaMaFvx8cxXcJVJ5kU9csMeD/JN8on7ybassU8rY=
gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1 h1:E3uJqX6ImJL9AFdjGbiW04jq8IQ+NcOK+JSiWq2TbRw=
gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg= nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=

View file

@ -40,6 +40,9 @@ type Session struct {
RawMessages bool `yaml:":rawmessages"` RawMessages bool `yaml:":rawmessages"`
AsciiArrows bool `yaml:":asciiarrows"` AsciiArrows bool `yaml:":asciiarrows"`
MUC bool `yaml:":muc"` MUC bool `yaml:":muc"`
OOBMode bool `yaml:":oobmode"`
Carbons bool `yaml:":carbons"`
HideIds bool `yaml:":hideids"`
} }
var configKeys = []string{ var configKeys = []string{
@ -48,6 +51,9 @@ var configKeys = []string{
"rawmessages", "rawmessages",
"asciiarrows", "asciiarrows",
"muc", "muc",
"oobmode",
"carbons",
"hideids",
} }
var sessionDB *SessionsYamlDB var sessionDB *SessionsYamlDB
@ -122,6 +128,12 @@ func (s *Session) Get(key string) (string, error) {
return fromBool(s.AsciiArrows), nil return fromBool(s.AsciiArrows), nil
case "muc": case "muc":
return fromBool(s.MUC), nil return fromBool(s.MUC), nil
case "oobmode":
return fromBool(s.OOBMode), nil
case "carbons":
return fromBool(s.Carbons), nil
case "hideids":
return fromBool(s.HideIds), nil
} }
return "", errors.New("Unknown session property") return "", errors.New("Unknown session property")
@ -172,6 +184,27 @@ func (s *Session) Set(key string, value string) (string, error) {
} }
s.MUC = b s.MUC = b
return value, nil return value, nil
case "oobmode":
b, err := toBool(value)
if err != nil {
return "", err
}
s.OOBMode = b
return value, nil
case "carbons":
b, err := toBool(value)
if err != nil {
return "", err
}
s.Carbons = b
return value, nil
case "hideids":
b, err := toBool(value)
if err != nil {
return "", err
}
s.HideIds = b
return value, nil
} }
return "", errors.New("Unknown session property") return "", errors.New("Unknown session property")

View file

@ -48,6 +48,7 @@ func TestSessionToMap(t *testing.T) {
Timezone: "klsf", Timezone: "klsf",
RawMessages: true, RawMessages: true,
MUC: true, MUC: true,
OOBMode: true,
} }
m := session.ToMap() m := session.ToMap()
sample := map[string]string{ sample := map[string]string{
@ -56,6 +57,9 @@ func TestSessionToMap(t *testing.T) {
"muc": "true", "muc": "true",
"rawmessages": "true", "rawmessages": "true",
"asciiarrows": "false", "asciiarrows": "false",
"oobmode": "true",
"carbons": "false",
"hideids": "false",
} }
if !reflect.DeepEqual(m, sample) { if !reflect.DeepEqual(m, sample) {
t.Errorf("Map does not match the sample: %v", m) t.Errorf("Map does not match the sample: %v", m)

View file

@ -15,7 +15,8 @@ import (
goxmpp "gosrc.io/xmpp" goxmpp "gosrc.io/xmpp"
) )
const version string = "1.2.1" var version string = "2.0.0-dev"
var commit string
var sm *goxmpp.StreamManager var sm *goxmpp.StreamManager
var component *goxmpp.Component var component *goxmpp.Component
@ -25,11 +26,17 @@ var cleanupDone chan struct{}
var sigintChannel chan os.Signal var sigintChannel chan os.Signal
func main() { func main() {
if commit != "" {
version = fmt.Sprintf("%v-%v", version, commit)
}
var profilingPort = flag.Int("profiling-port", 0, "The port for pprof server") var profilingPort = flag.Int("profiling-port", 0, "The port for pprof server")
// YAML config, compatible with the format of Zhabogram 2.0.0 // YAML config, compatible with the format of Zhabogram 2.0.0
var configPath = flag.String("config", "config.yml", "Config file path") var configPath = flag.String("config", "config.yml", "Config file path")
// JSON schema (not for editing by a user) // JSON schema (not for editing by a user)
var schemaPath = flag.String("schema", "./config_schema.json", "Schema file path") var schemaPath = flag.String("schema", "./config_schema.json", "Schema file path")
// Folder for Badger DB of message ids
var idsPath = flag.String("ids", "ids", "Ids folder path")
var versionFlag = flag.Bool("version", false, "Print the version and exit") var versionFlag = flag.Bool("version", false, "Print the version and exit")
flag.Parse() flag.Parse()
@ -55,7 +62,9 @@ func main() {
SetLogrusLevel(config.XMPP.Loglevel) SetLogrusLevel(config.XMPP.Loglevel)
sm, component, err = xmpp.NewComponent(config.XMPP, config.Telegram) log.Infof("Starting telegabber version %v", version)
sm, component, err = xmpp.NewComponent(config.XMPP, config.Telegram, *idsPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View file

@ -133,3 +133,13 @@ func (cache *Cache) SetStatus(id int64, show string, status string) {
Description: status, Description: status,
} }
} }
// Destruct splits a cached status into show, description and type
func (status *Status) Destruct() (show, description, typ string) {
show, description = status.XMPP, status.Description
if show == "unavailable" {
typ = show
show = ""
}
return
}

View file

@ -2,6 +2,7 @@ package telegram
import ( import (
"github.com/pkg/errors" "github.com/pkg/errors"
"hash/maphash"
"path/filepath" "path/filepath"
"strconv" "strconv"
"sync" "sync"
@ -44,7 +45,7 @@ type DelayedStatus struct {
type Client struct { type Client struct {
client *client.Client client *client.Client
authorizer *clientAuthorizer authorizer *clientAuthorizer
parameters *client.TdlibParameters parameters *client.SetTdlibParametersRequest
options []client.Option options []client.Option
me *client.User me *client.User
@ -52,6 +53,7 @@ type Client struct {
jid string jid string
Session *persistence.Session Session *persistence.Session
resources map[string]bool resources map[string]bool
outbox map[string]string
content *config.TelegramContentConfig content *config.TelegramContentConfig
cache *cache.Cache cache *cache.Cache
online bool online bool
@ -59,13 +61,22 @@ type Client struct {
DelayedStatuses map[int64]*DelayedStatus DelayedStatuses map[int64]*DelayedStatus
DelayedStatusesLock sync.Mutex DelayedStatusesLock sync.Mutex
locks clientLocks lastMsgHashes map[int64]uint64
msgHashSeed maphash.Seed
locks clientLocks
SendMessageLock sync.Mutex
} }
type clientLocks struct { type clientLocks struct {
authorizationReady sync.WaitGroup authorizationReady sync.Mutex
chatMessageLocks map[int64]*sync.Mutex chatMessageLocks map[int64]*sync.Mutex
resourcesLock sync.Mutex resourcesLock sync.Mutex
outboxLock sync.Mutex
lastMsgHashesLock sync.Mutex
authorizerReadLock sync.Mutex
authorizerWriteLock sync.Mutex
} }
// NewClient instantiates a Telegram App // NewClient instantiates a Telegram App
@ -92,7 +103,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component
datadir = "./sessions/" // ye olde defaute datadir = "./sessions/" // ye olde defaute
} }
parameters := client.TdlibParameters{ parameters := client.SetTdlibParametersRequest{
UseTestDc: false, UseTestDc: false,
DatabaseDirectory: filepath.Join(datadir, jid), DatabaseDirectory: filepath.Join(datadir, jid),
@ -121,10 +132,13 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component
jid: jid, jid: jid,
Session: session, Session: session,
resources: make(map[string]bool), resources: make(map[string]bool),
outbox: make(map[string]string),
content: &conf.Content, content: &conf.Content,
cache: cache.NewCache(), cache: cache.NewCache(),
options: options, options: options,
DelayedStatuses: make(map[int64]*DelayedStatus), DelayedStatuses: make(map[int64]*DelayedStatus),
lastMsgHashes: make(map[int64]uint64),
msgHashSeed: maphash.MakeSeed(),
locks: clientLocks{ locks: clientLocks{
chatMessageLocks: make(map[int64]*sync.Mutex), chatMessageLocks: make(map[int64]*sync.Mutex),
}, },

View file

@ -15,11 +15,11 @@ import (
) )
const notEnoughArguments string = "Not enough arguments" const notEnoughArguments string = "Not enough arguments"
const telegramNotInitialized string = "Telegram connection is not initialized yet" const TelegramNotInitialized string = "Telegram connection is not initialized yet"
const TelegramAuthDone string = "Authorization is done already"
const notOnline string = "Not online" const notOnline string = "Not online"
var permissionsAdmin = client.ChatMemberStatusAdministrator{ var permissionsAdmin = client.ChatAdministratorRights{
CanBeEdited: true,
CanChangeInfo: true, CanChangeInfo: true,
CanPostMessages: true, CanPostMessages: true,
CanEditMessages: true, CanEditMessages: true,
@ -30,20 +30,27 @@ var permissionsAdmin = client.ChatMemberStatusAdministrator{
CanPromoteMembers: false, CanPromoteMembers: false,
} }
var permissionsMember = client.ChatPermissions{ var permissionsMember = client.ChatPermissions{
CanSendMessages: true, CanSendBasicMessages: true,
CanSendMediaMessages: true, CanSendAudios: true,
CanSendDocuments: true,
CanSendPhotos: true,
CanSendVideos: true,
CanSendVideoNotes: true,
CanSendVoiceNotes: true,
CanSendPolls: true, CanSendPolls: true,
CanSendOtherMessages: true, CanSendOtherMessages: true,
CanAddWebPagePreviews: true, CanAddWebPagePreviews: true,
CanChangeInfo: true, CanChangeInfo: true,
CanInviteUsers: true, CanInviteUsers: true,
CanPinMessages: true, CanPinMessages: true,
CanManageTopics: true,
} }
var permissionsReadonly = client.ChatPermissions{} var permissionsReadonly = client.ChatPermissions{}
var transportCommands = map[string]command{ var transportCommands = map[string]command{
"login": command{"phone", "sign in"}, "login": command{"phone", "sign in"},
"logout": command{"", "sign out"}, "logout": command{"", "sign out"},
"cancelauth": command{"", "quit the signin wizard"},
"code": command{"", "check one-time code"}, "code": command{"", "check one-time code"},
"password": command{"", "check 2fa password"}, "password": command{"", "check 2fa password"},
"setusername": command{"", "update @username"}, "setusername": command{"", "update @username"},
@ -52,6 +59,10 @@ var transportCommands = map[string]command{
"setpassword": command{"[old] [new]", "set or remove password"}, "setpassword": command{"[old] [new]", "set or remove password"},
"config": command{"[param] [value]", "view or update configuration options"}, "config": command{"[param] [value]", "view or update configuration options"},
"report": command{"[chat] [comment]", "report a chat by id or @username"}, "report": command{"[chat] [comment]", "report a chat by id or @username"},
"add": command{"@username", "add @username to your chat list"},
"join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"},
"supergroup": command{"title description", "create new supergroup «title» with «description»"},
"channel": command{"title description", "create new channel «title» with «description»"},
} }
var chatCommands = map[string]command{ var chatCommands = map[string]command{
@ -60,6 +71,7 @@ var chatCommands = map[string]command{
"silent": command{"message", "send a message without sound"}, "silent": command{"message", "send a message without sound"},
"schedule": command{"{online | 2006-01-02T15:04:05 | 15:04:05} message", "schedules a message either to timestamp or to whenever the user goes online"}, "schedule": command{"{online | 2006-01-02T15:04:05 | 15:04:05} message", "schedules a message either to timestamp or to whenever the user goes online"},
"forward": command{"message_id target_chat", "forwards a message"}, "forward": command{"message_id target_chat", "forwards a message"},
"vcard": command{"", "print vCard as text"},
"add": command{"@username", "add @username to your chat list"}, "add": command{"@username", "add @username to your chat list"},
"join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"}, "join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"},
"group": command{"title", "create groupchat «title» with current user"}, "group": command{"title", "create groupchat «title» with current user"},
@ -168,6 +180,10 @@ func rawCmdArguments(cmdline string, start uint8) string {
return "" return ""
} }
func keyValueString(key, value string) string {
return fmt.Sprintf("%s: %s", key, value)
}
func (c *Client) unsubscribe(chatID int64) error { func (c *Client) unsubscribe(chatID int64) error {
return gateway.SendPresence( return gateway.SendPresence(
c.xmpp, c.xmpp,
@ -179,11 +195,17 @@ func (c *Client) unsubscribe(chatID int64) error {
func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) { func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) {
for i := len(messages) - 1; i >= 0; i-- { for i := len(messages) - 1; i >= 0; i-- {
message := messages[i]
reply, _ := c.getMessageReply(message)
gateway.SendMessage( gateway.SendMessage(
c.jid, c.jid,
strconv.FormatInt(chatID, 10), strconv.FormatInt(chatID, 10),
c.formatMessage(0, 0, false, messages[i]), c.formatMessage(0, 0, false, message),
strconv.FormatInt(message.Id, 10),
c.xmpp, c.xmpp,
reply,
false,
) )
} }
} }
@ -215,7 +237,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
switch cmd { switch cmd {
case "login", "code", "password": case "login", "code", "password":
if cmd == "login" && c.Session.Login != "" { if cmd == "login" && c.Session.Login != "" {
return "" return "Phone number already provided, use /cancelauth to start over"
} }
if len(args) < 1 { if len(args) < 1 {
@ -223,36 +245,35 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
} }
if cmd == "login" { if cmd == "login" {
wasSessionLoginEmpty := c.Session.Login == "" err := c.TryLogin(resource, args[0])
c.Session.Login = args[0] if err != nil {
return err.Error()
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: the command just needs to be resent again
time.Sleep(1e5)
} }
}
if c.authorizer == nil { c.locks.authorizerWriteLock.Lock()
return telegramNotInitialized defer c.locks.authorizerWriteLock.Unlock()
}
switch cmd {
// sign in
case "login":
c.authorizer.PhoneNumber <- args[0] c.authorizer.PhoneNumber <- args[0]
// check auth code } else {
case "code": c.locks.authorizerWriteLock.Lock()
c.authorizer.Code <- args[0] defer c.locks.authorizerWriteLock.Unlock()
// check auth password
case "password": if c.authorizer == nil {
c.authorizer.Password <- args[0] return TelegramNotInitialized
}
if c.authorizer.isClosed {
return TelegramAuthDone
}
switch cmd {
// check auth code
case "code":
c.authorizer.Code <- args[0]
// check auth password
case "password":
c.authorizer.Password <- args[0]
}
} }
// sign out // sign out
case "logout": case "logout":
@ -271,6 +292,13 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
} }
c.Session.Login = "" c.Session.Login = ""
// cancel auth
case "cancelauth":
if c.Online() {
return "Not allowed when online, use /logout instead"
}
c.cancelAuth()
return "Cancelled"
// set @username // set @username
case "setusername": case "setusername":
if !c.Online() { if !c.Online() {
@ -290,25 +318,36 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
} }
// set My Name // set My Name
case "setname": case "setname":
if !c.Online() {
return notOnline
}
var firstname string var firstname string
var lastname string var lastname string
if len(args) > 0 { if len(args) > 0 {
firstname = args[0] firstname = args[0]
} }
if firstname == "" {
return "The name should contain at least one character"
}
if len(args) > 1 { if len(args) > 1 {
lastname = args[1] lastname = rawCmdArguments(cmdline, 1)
} }
_, err := c.client.SetName(&client.SetNameRequest{ c.locks.authorizerWriteLock.Lock()
FirstName: firstname, if c.authorizer != nil && !c.authorizer.isClosed {
LastName: lastname, c.authorizer.FirstName <- firstname
}) c.authorizer.LastName <- lastname
if err != nil { c.locks.authorizerWriteLock.Unlock()
return errors.Wrap(err, "Couldn't set name").Error() } else {
c.locks.authorizerWriteLock.Unlock()
if !c.Online() {
return notOnline
}
_, err := c.client.SetName(&client.SetNameRequest{
FirstName: firstname,
LastName: lastname,
})
if err != nil {
return errors.Wrap(err, "Couldn't set name").Error()
}
} }
// set About // set About
case "setbio": case "setbio":
@ -344,6 +383,10 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
} }
case "config": case "config":
if len(args) > 1 { if len(args) > 1 {
if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" {
return "The server did not allow to enable carbons"
}
value, err := c.Session.Set(args[0], args[1]) value, err := c.Session.Set(args[0], args[1])
if err != nil { if err != nil {
return err.Error() return err.Error()
@ -387,6 +430,14 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
} else { } else {
return "Reported" return "Reported"
} }
case "add":
return c.cmdAdd(args)
case "join":
return c.cmdJoin(args)
case "supergroup":
return c.cmdSupergroup(args, cmdline)
case "channel":
return c.cmdChannel(args, cmdline)
case "help": case "help":
return helpString(helpTypeTransport) return helpString(helpTypeTransport)
} }
@ -463,14 +514,17 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
return "Last message is empty", true return "Last message is empty", true
} }
content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "") content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0))
if content != nil { if content != nil {
c.client.EditMessageText(&client.EditMessageTextRequest{ _, err = c.client.EditMessageText(&client.EditMessageTextRequest{
ChatId: chatID, ChatId: chatID,
MessageId: message.Id, MessageId: message.Id,
InputMessageContent: content, InputMessageContent: content,
}) })
if err != nil {
return "Message editing error", true
}
} else { } else {
return "Message processing error", true return "Message processing error", true
} }
@ -480,7 +534,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
return "Not enough arguments", true return "Not enough arguments", true
} }
content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "") content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0))
if content != nil { if content != nil {
_, err := c.client.SendMessage(&client.SendMessageRequest{ _, err := c.client.SendMessage(&client.SendMessageRequest{
@ -559,7 +613,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
} }
} }
content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 1), "") content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 1))
if content != nil { if content != nil {
_, err := c.client.SendMessage(&client.SendMessageRequest{ _, err := c.client.SendMessage(&client.SendMessageRequest{
@ -606,80 +660,33 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
c.ProcessIncomingMessage(targetChatId, message) c.ProcessIncomingMessage(targetChatId, message)
} }
} }
// print vCard
case "vcard":
info, err := c.GetVcardInfo(chatID)
if err != nil {
return err.Error(), true
}
_, link := c.PermastoreFile(info.Photo, true)
entries := []string{
keyValueString("Chat title", info.Fn),
keyValueString("Photo", link),
keyValueString("Usernames", c.usernamesToString(info.Nicknames)),
keyValueString("Full name", info.Given+" "+info.Family),
keyValueString("Phone number", info.Tel),
}
return strings.Join(entries, "\n"), true
// add @contact // add @contact
case "add": case "add":
if len(args) < 1 { return c.cmdAdd(args), true
return notEnoughArguments, true
}
chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{
Username: args[0],
})
if err != nil {
return err.Error(), true
}
if chat == nil {
return "No error, but chat is nil", true
}
c.subscribeToID(chat.Id, chat)
// join https://t.me/publichat or @publicchat // join https://t.me/publichat or @publicchat
case "join": case "join":
if len(args) < 1 { return c.cmdJoin(args), true
return notEnoughArguments, true
}
if strings.HasPrefix(args[0], "@") {
chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{
Username: args[0],
})
if err != nil {
return err.Error(), true
}
if chat == nil {
return "No error, but chat is nil", true
}
_, err = c.client.JoinChat(&client.JoinChatRequest{
ChatId: chat.Id,
})
if err != nil {
return err.Error(), true
}
} else {
_, err := c.client.JoinChatByInviteLink(&client.JoinChatByInviteLinkRequest{
InviteLink: args[0],
})
if err != nil {
return err.Error(), true
}
}
// create new supergroup // create new supergroup
case "supergroup": case "supergroup":
if len(args) < 1 { return c.cmdSupergroup(args, cmdline), true
return notEnoughArguments, true
}
_, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{
Title: args[0],
Description: rawCmdArguments(cmdline, 1),
})
if err != nil {
return err.Error(), true
}
// create new channel // create new channel
case "channel": case "channel":
if len(args) < 1 { return c.cmdChannel(args, cmdline), true
return notEnoughArguments, true
}
_, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{
Title: args[0],
Description: rawCmdArguments(cmdline, 1),
IsChannel: true,
})
if err != nil {
return err.Error(), true
}
// create new secret chat with current user // create new secret chat with current user
case "secret": case "secret":
_, err := c.client.CreateNewSecretChat(&client.CreateNewSecretChatRequest{ _, err := c.client.CreateNewSecretChat(&client.CreateNewSecretChatRequest{
@ -880,7 +887,10 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
} }
// clone the permissions // clone the permissions
status := permissionsAdmin status := client.ChatMemberStatusAdministrator{
CanBeEdited: true,
Rights: &permissionsAdmin,
}
if len(args) > 1 { if len(args) > 1 {
status.CustomTitle = args[1] status.CustomTitle = args[1]
@ -930,9 +940,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
return "Invalid TTL", true return "Invalid TTL", true
} }
} }
_, err = c.client.SetChatMessageTtl(&client.SetChatMessageTtlRequest{ _, err = c.client.SetChatMessageAutoDeleteTime(&client.SetChatMessageAutoDeleteTimeRequest{
ChatId: chatID, ChatId: chatID,
Ttl: int32(ttl), MessageAutoDeleteTime: int32(ttl),
}) })
if err != nil { if err != nil {
@ -1076,3 +1086,89 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
return "", true return "", true
} }
func (c *Client) cmdAdd(args []string) string {
if len(args) < 1 {
return notEnoughArguments
}
chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{
Username: args[0],
})
if err != nil {
return err.Error()
}
if chat == nil {
return "No error, but chat is nil"
}
c.subscribeToID(chat.Id, chat)
return ""
}
func (c *Client) cmdJoin(args []string) string {
if len(args) < 1 {
return notEnoughArguments
}
if strings.HasPrefix(args[0], "@") {
chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{
Username: args[0],
})
if err != nil {
return err.Error()
}
if chat == nil {
return "No error, but chat is nil"
}
_, err = c.client.JoinChat(&client.JoinChatRequest{
ChatId: chat.Id,
})
if err != nil {
return err.Error()
}
} else {
_, err := c.client.JoinChatByInviteLink(&client.JoinChatByInviteLinkRequest{
InviteLink: args[0],
})
if err != nil {
return err.Error()
}
}
return ""
}
func (c *Client) cmdSupergroup(args []string, cmdline string) string {
if len(args) < 1 {
return notEnoughArguments
}
_, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{
Title: args[0],
Description: rawCmdArguments(cmdline, 1),
})
if err != nil {
return err.Error()
}
return ""
}
func (c *Client) cmdChannel(args []string, cmdline string) string {
if len(args) < 1 {
return notEnoughArguments
}
_, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{
Title: args[0],
Description: rawCmdArguments(cmdline, 1),
IsChannel: true,
})
if err != nil {
return err.Error()
}
return ""
}

View file

@ -3,6 +3,7 @@ package telegram
import ( import (
"github.com/pkg/errors" "github.com/pkg/errors"
"strconv" "strconv"
"time"
"dev.narayana.im/narayana/telegabber/xmpp/gateway" "dev.narayana.im/narayana/telegabber/xmpp/gateway"
@ -13,25 +14,25 @@ import (
const chatsLimit int32 = 999 const chatsLimit int32 = 999
type clientAuthorizer struct { type clientAuthorizer struct {
TdlibParameters chan *client.TdlibParameters TdlibParameters chan *client.SetTdlibParametersRequest
PhoneNumber chan string PhoneNumber chan string
Code chan string Code chan string
State chan client.AuthorizationState State chan client.AuthorizationState
Password chan string Password chan string
FirstName chan string
LastName chan string
isClosed bool
} }
func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.AuthorizationState) error { func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.AuthorizationState) error {
if stateHandler.isClosed {
return errors.New("Channel is closed")
}
stateHandler.State <- state stateHandler.State <- state
switch state.AuthorizationStateType() { switch state.AuthorizationStateType() {
case client.TypeAuthorizationStateWaitTdlibParameters: case client.TypeAuthorizationStateWaitTdlibParameters:
_, err := c.SetTdlibParameters(&client.SetTdlibParametersRequest{ _, err := c.SetTdlibParameters(<-stateHandler.TdlibParameters)
Parameters: <-stateHandler.TdlibParameters,
})
return err
case client.TypeAuthorizationStateWaitEncryptionKey:
_, err := c.CheckDatabaseEncryptionKey(&client.CheckDatabaseEncryptionKeyRequest{})
return err return err
case client.TypeAuthorizationStateWaitPhoneNumber: case client.TypeAuthorizationStateWaitPhoneNumber:
@ -52,7 +53,11 @@ func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.Auth
return err return err
case client.TypeAuthorizationStateWaitRegistration: case client.TypeAuthorizationStateWaitRegistration:
return client.ErrNotSupportedAuthorizationState _, err := c.RegisterUser(&client.RegisterUserRequest{
FirstName: <-stateHandler.FirstName,
LastName: <-stateHandler.LastName,
})
return err
case client.TypeAuthorizationStateWaitPassword: case client.TypeAuthorizationStateWaitPassword:
_, err := c.CheckAuthenticationPassword(&client.CheckAuthenticationPasswordRequest{ _, err := c.CheckAuthenticationPassword(&client.CheckAuthenticationPasswordRequest{
@ -77,42 +82,54 @@ func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.Auth
} }
func (stateHandler *clientAuthorizer) Close() { func (stateHandler *clientAuthorizer) Close() {
if stateHandler.isClosed {
return
}
stateHandler.isClosed = true
close(stateHandler.TdlibParameters) close(stateHandler.TdlibParameters)
close(stateHandler.PhoneNumber) close(stateHandler.PhoneNumber)
close(stateHandler.Code) close(stateHandler.Code)
close(stateHandler.State) close(stateHandler.State)
close(stateHandler.Password) close(stateHandler.Password)
close(stateHandler.FirstName)
close(stateHandler.LastName)
} }
// Connect starts TDlib connection // Connect starts TDlib connection
func (c *Client) Connect(resource string) error { func (c *Client) Connect(resource string) error {
log.Warn("Attempting to connect to Telegram network...")
// avoid conflict if another authorization is pending already // avoid conflict if another authorization is pending already
c.locks.authorizationReady.Wait() c.locks.authorizationReady.Lock()
if c.Online() { if c.Online() {
c.roster(resource) c.roster(resource)
c.locks.authorizationReady.Unlock()
return nil return nil
} }
log.Warn("Connecting to Telegram network...") log.Warn("Connecting to Telegram network...")
c.locks.authorizerWriteLock.Lock()
c.authorizer = &clientAuthorizer{ c.authorizer = &clientAuthorizer{
TdlibParameters: make(chan *client.TdlibParameters, 1), TdlibParameters: make(chan *client.SetTdlibParametersRequest, 1),
PhoneNumber: make(chan string, 1), PhoneNumber: make(chan string, 1),
Code: make(chan string, 1), Code: make(chan string, 1),
State: make(chan client.AuthorizationState, 10), State: make(chan client.AuthorizationState, 10),
Password: make(chan string, 1), Password: make(chan string, 1),
FirstName: make(chan string, 1),
LastName: make(chan string, 1),
} }
c.locks.authorizationReady.Add(1)
go c.interactor() go c.interactor()
log.Warn("Interactor launched")
c.authorizer.TdlibParameters <- c.parameters c.authorizer.TdlibParameters <- c.parameters
c.locks.authorizerWriteLock.Unlock()
tdlibClient, err := client.NewClient(c.authorizer, c.options...) tdlibClient, err := client.NewClient(c.authorizer, c.options...)
if err != nil { if err != nil {
c.locks.authorizationReady.Done() c.locks.authorizationReady.Unlock()
return errors.Wrap(err, "Couldn't initialize a Telegram client instance") return errors.Wrap(err, "Couldn't initialize a Telegram client instance")
} }
@ -130,7 +147,7 @@ func (c *Client) Connect(resource string) error {
go c.updateHandler() go c.updateHandler()
c.online = true c.online = true
c.locks.authorizationReady.Done() c.locks.authorizationReady.Unlock()
c.addResource(resource) c.addResource(resource)
go func() { go func() {
@ -141,14 +158,55 @@ func (c *Client) Connect(resource string) error {
log.Errorf("Could not retrieve chats: %v", err) log.Errorf("Could not retrieve chats: %v", err)
} }
gateway.SendPresence(c.xmpp, c.jid, gateway.SPType("subscribe")) gateway.SubscribeToTransport(c.xmpp, c.jid)
gateway.SendPresence(c.xmpp, c.jid, gateway.SPType("subscribed"))
gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login)) gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login))
}() }()
return nil 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 // Disconnect drops TDlib connection and
// returns the flag indicating if disconnecting is permitted // returns the flag indicating if disconnecting is permitted
func (c *Client) Disconnect(resource string, quit bool) bool { func (c *Client) Disconnect(resource string, quit bool) bool {
@ -178,20 +236,23 @@ func (c *Client) Disconnect(resource string, quit bool) bool {
) )
} }
_, err := c.client.Close() c.close()
if err != nil {
log.Errorf("Couldn't close the Telegram instance: %v; %#v", err, c)
}
c.forceClose()
return true return true
} }
func (c *Client) interactor() { func (c *Client) interactor() {
for { 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 state, ok := <-c.authorizer.State
if !ok { if !ok {
log.Warn("Interactor is disconnected") log.Warn("Interactor is disconnected")
c.locks.authorizerReadLock.Unlock()
return return
} }
@ -206,25 +267,56 @@ func (c *Client) interactor() {
if c.Session.Login != "" { if c.Session.Login != "" {
c.authorizer.PhoneNumber <- c.Session.Login c.authorizer.PhoneNumber <- c.Session.Login
} else { } else {
gateway.SendMessage(c.jid, "", "Please, enter your Telegram login via /login 12345", c.xmpp) gateway.SendServiceMessage(c.jid, "Please, enter your Telegram login via /login 12345", c.xmpp)
} }
// stage 1: wait for auth code // stage 1: wait for auth code
case client.TypeAuthorizationStateWaitCode: case client.TypeAuthorizationStateWaitCode:
log.Warn("Waiting for authorization code...") log.Warn("Waiting for authorization code...")
gateway.SendMessage(c.jid, "", "Please, enter authorization code via /code 12345", c.xmpp) 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 // stage 2: wait for 2fa
case client.TypeAuthorizationStateWaitPassword: case client.TypeAuthorizationStateWaitPassword:
log.Warn("Waiting for 2FA password...") log.Warn("Waiting for 2FA password...")
gateway.SendMessage(c.jid, "", "Please, enter 2FA passphrase via /password 12345", c.xmpp) gateway.SendServiceMessage(c.jid, "Please, enter 2FA passphrase via /password 12345", c.xmpp)
} }
c.locks.authorizerReadLock.Unlock()
} }
} }
func (c *Client) forceClose() { 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.online = false
c.authorizer = nil 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 // Online checks if the updates listener is alive
func (c *Client) Online() bool { func (c *Client) Online() bool {
return c.online return c.online

View file

@ -203,11 +203,11 @@ func (c *Client) updateChatLastMessage(update *client.UpdateChatLastMessage) {
// message received // message received
func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { func (c *Client) updateNewMessage(update *client.UpdateNewMessage) {
go func() { chatId := update.Message.ChatId
chatId := update.Message.ChatId
// guarantee sequential message delivering per chat // guarantee sequential message delivering per chat
lock := c.getChatMessageLock(chatId) lock := c.getChatMessageLock(chatId)
go func() {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
@ -223,13 +223,35 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) {
}).Warn("New message from chat") }).Warn("New message from chat")
c.ProcessIncomingMessage(chatId, update.Message) c.ProcessIncomingMessage(chatId, update.Message)
c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content)
}() }()
} }
// message content updated // message content updated
func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
markupFunction := formatter.EntityToXEP0393 markupFunction := c.getFormatter()
if update.NewContent.MessageContentType() == client.TypeMessageText {
defer c.updateLastMessageHash(update.ChatId, update.MessageId, update.NewContent)
c.SendMessageLock.Lock()
c.SendMessageLock.Unlock()
xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId)
var ignoredResource string
if err == nil {
ignoredResource = c.popFromOutbox(xmppId)
} else {
log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.MessageId)
}
log.Infof("ignoredResource: %v", ignoredResource)
jids := c.getCarbonFullJids(true, ignoredResource)
if len(jids) == 0 {
log.Info("The only resource is ignored, aborting")
return
}
if update.NewContent.MessageContentType() == client.TypeMessageText && c.hasLastMessageHashChanged(update.ChatId, update.MessageId, update.NewContent) {
textContent := update.NewContent.(*client.MessageText) textContent := update.NewContent.(*client.MessageText)
var editChar string var editChar string
if c.Session.AsciiArrows { if c.Session.AsciiArrows {
@ -242,7 +264,9 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
textContent.Text.Entities, textContent.Text.Entities,
markupFunction, markupFunction,
)) ))
gateway.SendMessage(c.jid, strconv.FormatInt(update.ChatId, 10), text, c.xmpp) for _, jid := range jids {
gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, false)
}
} }
} }
@ -256,7 +280,7 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) {
deleteChar = "✗ " deleteChar = "✗ "
} }
text := deleteChar + strings.Join(int64SliceToStringSlice(update.MessageIds), ",") text := deleteChar + strings.Join(int64SliceToStringSlice(update.MessageIds), ",")
gateway.SendMessage(c.jid, strconv.FormatInt(update.ChatId, 10), text, c.xmpp) gateway.SendTextMessage(c.jid, strconv.FormatInt(update.ChatId, 10), text, c.xmpp)
} }
} }
@ -272,6 +296,11 @@ func (c *Client) updateAuthorizationState(update *client.UpdateAuthorizationStat
// clean uploaded files // clean uploaded files
func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) { func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) {
log.Debugf("replace message %v with %v", update.OldMessageId, update.Message.Id)
if err := gateway.IdsDB.ReplaceTgId(c.Session.Login, c.jid, update.Message.ChatId, update.OldMessageId, update.Message.Id); err != nil {
log.Errorf("failed to replace %v with %v: %v", update.OldMessageId, update.Message.Id, err.Error())
}
file, _ := c.contentToFile(update.Message.Content) file, _ := c.contentToFile(update.Message.Content)
if file != nil && file.Local != nil { if file != nil && file.Local != nil {
c.cleanTempFile(file.Local.Path) c.cleanTempFile(file.Local.Path)
@ -289,8 +318,13 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) {
gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp) gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp)
// set also the status (for group chats only) // set also the status (for group chats only)
_, user, _ := c.GetContactByID(update.ChatId, nil) chat, user, _ := c.GetContactByID(update.ChatId, nil)
if user == nil { if user == nil {
c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true)) c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true))
} }
// update chat title in the cache
if chat != nil {
chat.Title = update.Title
}
} }

View file

@ -2,8 +2,10 @@ package telegram
import ( import (
"crypto/sha1" "crypto/sha1"
"encoding/binary"
"fmt" "fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
"hash/maphash"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -24,12 +26,23 @@ import (
"github.com/zelenin/go-tdlib/client" "github.com/zelenin/go-tdlib/client"
) )
type VCardInfo struct {
Fn string
Photo *client.File
Nicknames []string
Given string
Family string
Tel string
Info string
}
var errOffline = errors.New("TDlib instance is offline") var errOffline = errors.New("TDlib instance is offline")
var spaceRegex = regexp.MustCompile(`\s+`) var spaceRegex = regexp.MustCompile(`\s+`)
var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n")
const newlineChar string = "\n" const newlineChar string = "\n"
const messageHeaderSeparator string = " | "
// GetContactByUsername resolves username to user id retrieves user and chat information // GetContactByUsername resolves username to user id retrieves user and chat information
func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) {
@ -108,6 +121,33 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli
return chat, user, nil return chat, user, nil
} }
// IsPM checks if a chat is PM
func (c *Client) IsPM(id int64) (bool, error) {
if !c.Online() || id == 0 {
return false, errOffline
}
var err error
chat, ok := c.cache.GetChat(id)
if !ok {
chat, err = c.client.GetChat(&client.GetChatRequest{
ChatId: id,
})
if err != nil {
return false, err
}
c.cache.SetChat(id, chat)
}
chatType := chat.Type.ChatTypeType()
if chatType == client.TypeChatTypePrivate || chatType == client.TypeChatTypeSecret {
return true, nil
}
return false, nil
}
func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) { func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) {
var show, textStatus, presenceType string var show, textStatus, presenceType string
@ -179,7 +219,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
var photo string var photo string
if chat != nil && chat.Photo != nil { if chat != nil && chat.Photo != nil {
file, path, err := c.OpenPhotoFile(chat.Photo.Small, 1) file, path, err := c.ForceOpenFile(chat.Photo.Small, 1)
if err == nil { if err == nil {
defer file.Close() defer file.Close()
@ -203,15 +243,33 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
cachedStatus, ok := c.cache.GetStatus(chatID) cachedStatus, ok := c.cache.GetStatus(chatID)
if status == "" { if status == "" {
if ok { if ok {
show, status = cachedStatus.XMPP, cachedStatus.Description var typ string
show, status, typ = cachedStatus.Destruct()
if presenceType == "" {
presenceType = typ
}
log.WithFields(log.Fields{
"show": show,
"status": status,
"presenceType": presenceType,
}).Debug("Cached status")
} else if user != nil && user.Status != nil { } else if user != nil && user.Status != nil {
show, status, presenceType = c.userStatusToText(user.Status, chatID) show, status, presenceType = c.userStatusToText(user.Status, chatID)
log.WithFields(log.Fields{
"show": show,
"status": status,
"presenceType": presenceType,
}).Debug("Status to text")
} else { } else {
show, status = "chat", chat.Title show, status = "chat", chat.Title
} }
} }
c.cache.SetStatus(chatID, show, status) cacheShow := show
if presenceType == "unavailable" {
cacheShow = presenceType
}
c.cache.SetStatus(chatID, cacheShow, status)
newArgs := []args.V{ newArgs := []args.V{
gateway.SPFrom(strconv.FormatInt(chatID, 10)), gateway.SPFrom(strconv.FormatInt(chatID, 10)),
@ -246,12 +304,15 @@ func (c *Client) formatContact(chatID int64) string {
if chat != nil { if chat != nil {
str = fmt.Sprintf("%s (%v)", chat.Title, chat.Id) str = fmt.Sprintf("%s (%v)", chat.Title, chat.Id)
} else if user != nil { } else if user != nil {
username := user.Username var usernames string
if username == "" { if user.Usernames != nil {
username = strconv.FormatInt(user.Id, 10) usernames = c.usernamesToString(user.Usernames.ActiveUsernames)
}
if usernames == "" {
usernames = strconv.FormatInt(user.Id, 10)
} }
str = fmt.Sprintf("%s %s (%v)", user.FirstName, user.LastName, username) str = fmt.Sprintf("%s %s (%v)", user.FirstName, user.LastName, usernames)
} else { } else {
str = strconv.FormatInt(chatID, 10) str = strconv.FormatInt(chatID, 10)
} }
@ -261,6 +322,50 @@ func (c *Client) formatContact(chatID int64) string {
return str return str
} }
func (c *Client) getSenderId(message *client.Message) (senderId int64) {
if message.SenderId != nil {
switch message.SenderId.MessageSenderType() {
case client.TypeMessageSenderUser:
senderUser, _ := message.SenderId.(*client.MessageSenderUser)
senderId = senderUser.UserId
case client.TypeMessageSenderChat:
senderChat, _ := message.SenderId.(*client.MessageSenderChat)
senderId = senderChat.ChatId
}
}
return
}
func (c *Client) formatSender(message *client.Message) string {
return c.formatContact(c.getSenderId(message))
}
func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, replyMsg *client.Message) {
if message.ReplyToMessageId != 0 {
var err error
replyMsg, err = c.client.GetMessage(&client.GetMessageRequest{
ChatId: message.ChatId,
MessageId: message.ReplyToMessageId,
})
if err != nil {
log.Errorf("<error fetching message: %s>", err.Error())
return
}
replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, message.ReplyToMessageId)
if err != nil {
replyId = strconv.FormatInt(message.ReplyToMessageId, 10)
}
reply = &gateway.Reply{
Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()),
Id: replyId,
}
}
return
}
func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, message *client.Message) string { func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, message *client.Message) string {
var err error var err error
if message == nil { if message == nil {
@ -279,18 +384,7 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess
var str strings.Builder var str strings.Builder
// add messageid and sender // add messageid and sender
var senderId int64 str.WriteString(fmt.Sprintf("%v | %s | ", message.Id, c.formatSender(message)))
if message.SenderId != nil {
switch message.SenderId.MessageSenderType() {
case client.TypeMessageSenderUser:
senderUser, _ := message.SenderId.(*client.MessageSenderUser)
senderId = senderUser.UserId
case client.TypeMessageSenderChat:
senderChat, _ := message.SenderId.(*client.MessageSenderChat)
senderId = senderChat.ChatId
}
}
str.WriteString(fmt.Sprintf("%v | %s | ", message.Id, c.formatContact(senderId)))
// add date // add date
if !preview { if !preview {
str.WriteString( str.WriteString(
@ -350,10 +444,24 @@ func (c *Client) formatForward(fwd *client.MessageForwardInfo) string {
return "Unknown forward type" return "Unknown forward type"
} }
func (c *Client) formatFile(file *client.File, compact bool) string { func (c *Client) formatFile(file *client.File, compact bool) (string, string) {
if file == nil {
return "", ""
}
src, link := c.PermastoreFile(file, false)
if compact {
return link, link
} else {
return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link), link
}
}
// PermastoreFile steals a file out of TDlib control into an independent shared directory
func (c *Client) PermastoreFile(file *client.File, clone bool) (string, string) {
log.Debugf("file: %#v", file) log.Debugf("file: %#v", file)
if file == nil || file.Local == nil || file.Remote == nil { if file == nil || file.Local == nil || file.Remote == nil {
return "" return "", ""
} }
gateway.StorageLock.Lock() gateway.StorageLock.Lock()
@ -367,7 +475,7 @@ func (c *Client) formatFile(file *client.File, compact bool) string {
_, err := os.Stat(src) _, err := os.Stat(src)
if err != nil { if err != nil {
log.Errorf("Cannot access source file: %v", err) log.Errorf("Cannot access source file: %v", err)
return "" return "", ""
} }
size64 := uint64(file.Size) size64 := uint64(file.Size)
@ -377,18 +485,57 @@ func (c *Client) formatFile(file *client.File, compact bool) string {
dest := c.content.Path + "/" + basename // destination path dest := c.content.Path + "/" + basename // destination path
link = c.content.Link + "/" + basename // download link link = c.content.Link + "/" + basename // download link
// move if clone {
err = os.Rename(src, dest) file, path, err := c.ForceOpenFile(file, 1)
if err != nil { if err == nil {
linkErr := err.(*os.LinkError) defer file.Close()
if linkErr.Err.Error() == "file exists" {
log.Warn(err.Error()) // mode
mode := os.FileMode(0644)
fi, err := os.Stat(path)
if err == nil {
mode = fi.Mode().Perm()
}
// create destination
tempFile, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
if err != nil {
pathErr := err.(*os.PathError)
if pathErr.Err.Error() == "file exists" {
log.Warn(err.Error())
return src, link
} else {
log.Errorf("File creation error: %v", err)
return "<ERROR>", ""
}
}
defer tempFile.Close()
// copy
_, err = io.Copy(tempFile, file)
if err != nil {
log.Errorf("File copying error: %v", err)
return "<ERROR>", ""
}
} else if path != "" {
log.Errorf("Source file does not exist: %v", path)
return "<ERROR>", ""
} else { } else {
log.Errorf("File moving error: %v", err) log.Errorf("PHOTO: %#v", err.Error())
return "<ERROR>" return "<ERROR>", ""
}
} else {
// move
err = os.Rename(src, dest)
if err != nil {
linkErr := err.(*os.LinkError)
if linkErr.Err.Error() == "file exists" {
log.Warn(err.Error())
} else {
log.Errorf("File moving error: %v", err)
return "<ERROR>", ""
}
} }
} }
gateway.CachedStorageSize += size64
// chown // chown
if c.content.User != "" { if c.content.User != "" {
@ -407,13 +554,12 @@ func (c *Client) formatFile(file *client.File, compact bool) string {
log.Errorf("Wrong user name for chown: %v", err) log.Errorf("Wrong user name for chown: %v", err)
} }
} }
// copy or move should have succeeded at this point
gateway.CachedStorageSize += size64
} }
if compact { return src, link
return link
} else {
return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link)
}
} }
func (c *Client) formatBantime(hours int64) int32 { func (c *Client) formatBantime(hours int64) int32 {
@ -441,7 +587,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string {
return "<empty message>" return "<empty message>"
} }
markupFunction := formatter.EntityToXEP0393 markupFunction := c.getFormatter()
switch message.Content.MessageContentType() { switch message.Content.MessageContentType() {
case client.TypeMessageSticker: case client.TypeMessageSticker:
sticker, _ := message.Content.(*client.MessageSticker) sticker, _ := message.Content.(*client.MessageSticker)
@ -612,6 +758,22 @@ func (c *Client) messageToText(message *client.Message, preview bool) string {
return strings.Join(rows, "\n") return strings.Join(rows, "\n")
} }
case client.TypeMessageChatSetMessageAutoDeleteTime:
ttl, _ := message.Content.(*client.MessageChatSetMessageAutoDeleteTime)
name := c.formatContact(ttl.FromUserId)
if name == "" {
if ttl.MessageAutoDeleteTime == 0 {
return "The self-destruct timer was disabled"
} else {
return fmt.Sprintf("The self-destruct timer was set to %v seconds", ttl.MessageAutoDeleteTime)
}
} else {
if ttl.MessageAutoDeleteTime == 0 {
return fmt.Sprintf("%s disabled the self-destruct timer", name)
} else {
return fmt.Sprintf("%s set the self-destruct timer to %v seconds", name, ttl.MessageAutoDeleteTime)
}
}
} }
return fmt.Sprintf("unknown message (%s)", message.Content.MessageContentType()) return fmt.Sprintf("unknown message (%s)", message.Content.MessageContentType())
@ -626,7 +788,7 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl
case client.TypeMessageSticker: case client.TypeMessageSticker:
sticker, _ := content.(*client.MessageSticker) sticker, _ := content.(*client.MessageSticker)
file := sticker.Sticker.Sticker file := sticker.Sticker.Sticker
if sticker.Sticker.IsAnimated && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil { if sticker.Sticker.Format.StickerFormatType() == client.TypeStickerFormatTgs && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil {
file = sticker.Sticker.Thumbnail.File file = sticker.Sticker.Thumbnail.File
} }
return file, nil return file, nil
@ -681,40 +843,62 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl
return nil, nil return nil, nil
} }
func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string) string { func (c *Client) countCharsInLines(lines *[]string) (count int) {
for _, line := range *lines {
count += len(line)
}
return
}
func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, replyMsg *client.Message) (string, int, int) {
isPM, err := c.IsPM(message.ChatId)
if err != nil {
log.Errorf("Could not determine if chat is PM: %v", err)
}
isCarbonsEnabled := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons
// with carbons, hide for all messages in PM and only for outgoing in group chats
hideSender := isCarbonsEnabled && (message.IsOutgoing || isPM)
var replyStart, replyEnd int
prefix := []string{} prefix := []string{}
// message direction // message direction
var directionChar string var directionChar string
if c.Session.AsciiArrows { if !hideSender {
if message.IsOutgoing { if c.Session.AsciiArrows {
directionChar = "> " if message.IsOutgoing {
directionChar = "> "
} else {
directionChar = "< "
}
} else { } else {
directionChar = "< " if message.IsOutgoing {
} directionChar = "➡ "
} else { } else {
if message.IsOutgoing { directionChar = "⬅ "
directionChar = "➡ " }
} else {
directionChar = "⬅ "
} }
} }
prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) if !isPM || !c.Session.HideIds {
prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10))
}
// show sender in group chats // show sender in group chats
if message.ChatId < 0 && message.SenderId != nil { if !hideSender {
var senderId int64 sender := c.formatSender(message)
switch message.SenderId.MessageSenderType() { if sender != "" {
case client.TypeMessageSenderUser: prefix = append(prefix, sender)
senderUser, _ := message.SenderId.(*client.MessageSenderUser)
senderId = senderUser.UserId
case client.TypeMessageSenderChat:
senderChat, _ := message.SenderId.(*client.MessageSenderChat)
senderId = senderChat.ChatId
} }
prefix = append(prefix, c.formatContact(senderId))
} }
// reply to // reply to
if message.ReplyToMessageId != 0 { if message.ReplyToMessageId != 0 {
prefix = append(prefix, "reply: "+c.formatMessage(message.ChatId, message.ReplyToMessageId, true, nil)) if len(prefix) > 0 {
replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator)
}
replyLine := "reply: " + c.formatMessage(message.ChatId, message.ReplyToMessageId, true, replyMsg)
prefix = append(prefix, replyLine)
replyEnd = replyStart + len(replyLine)
if len(prefix) > 0 {
replyEnd += len(messageHeaderSeparator)
}
} }
if message.ForwardInfo != nil { if message.ForwardInfo != nil {
prefix = append(prefix, "fwd: "+c.formatForward(message.ForwardInfo)) prefix = append(prefix, "fwd: "+c.formatForward(message.ForwardInfo))
@ -728,7 +912,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string,
prefix = append(prefix, "file: "+fileString) prefix = append(prefix, "file: "+fileString)
} }
return strings.Join(prefix, " | ") return strings.Join(prefix, messageHeaderSeparator), replyStart, replyEnd
} }
func (c *Client) ensureDownloadFile(file *client.File) *client.File { func (c *Client) ensureDownloadFile(file *client.File) *client.File {
@ -749,7 +933,13 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.File {
// ProcessIncomingMessage transfers a message to XMPP side and marks it as read on Telegram side // ProcessIncomingMessage transfers a message to XMPP side and marks it as read on Telegram side
func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
var text string isCarbon := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons && message.IsOutgoing
jids := c.getCarbonFullJids(isCarbon, "")
var text, oob, auxText string
reply, replyMsg := c.getMessageReply(message)
content := message.Content content := message.Content
if content != nil && content.MessageContentType() == client.TypeMessageChatChangePhoto { if content != nil && content.MessageContentType() == client.TypeMessageChatChangePhoto {
chat, err := c.client.GetChat(&client.GetChatRequest{ chat, err := c.client.GetChat(&client.GetChatRequest{
@ -764,27 +954,47 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
text = c.messageToText(message, false) text = c.messageToText(message, false)
// OTR support (I do not know why would you need it, seriously) // OTR support (I do not know why would you need it, seriously)
if !(strings.HasPrefix(text, "?OTR") || c.Session.RawMessages) { if !(strings.HasPrefix(text, "?OTR") || (c.Session.RawMessages && !c.Session.OOBMode)) {
file, preview := c.contentToFile(content) file, preview := c.contentToFile(content)
// download file and preview (if present) // download file and preview (if present)
file = c.ensureDownloadFile(file) file = c.ensureDownloadFile(file)
preview = c.ensureDownloadFile(preview) preview = c.ensureDownloadFile(preview)
var prefix strings.Builder previewName, _ := c.formatFile(preview, true)
prefix.WriteString(c.messageToPrefix(message, c.formatFile(preview, true), c.formatFile(file, false))) fileName, link := c.formatFile(file, false)
if text != "" {
// \n if it is groupchat and message is not empty oob = link
if chatId < 0 { if c.Session.OOBMode && oob != "" {
prefix.WriteString("\n") typ := message.Content.MessageContentType()
} else if chatId > 0 { if typ != client.TypeMessageSticker {
prefix.WriteString(" | ") auxText = text
}
text = oob
} else if !c.Session.RawMessages {
var newText strings.Builder
prefix, replyStart, replyEnd := c.messageToPrefix(message, previewName, fileName, replyMsg)
newText.WriteString(prefix)
if reply != nil {
reply.Start = uint64(replyStart)
reply.End = uint64(replyEnd)
} }
prefix.WriteString(text) if text != "" {
} // \n if it is groupchat and message is not empty
if prefix != "" {
if chatId < 0 {
newText.WriteString("\n")
} else if chatId > 0 {
newText.WriteString(" | ")
}
}
text = prefix.String() newText.WriteString(text)
}
text = newText.String()
}
} }
} }
@ -794,26 +1004,40 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
MessageIds: []int64{message.Id}, MessageIds: []int64{message.Id},
ForceRead: true, ForceRead: true,
}) })
// forward message to XMPP // forward message to XMPP
gateway.SendMessage(c.jid, strconv.FormatInt(chatId, 10), text, c.xmpp) sId := strconv.FormatInt(message.Id, 10)
sChatId := strconv.FormatInt(chatId, 10)
for _, jid := range jids {
gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, isCarbon)
if auxText != "" {
gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isCarbon)
}
}
} }
// ProcessOutgoingMessage executes commands or sends messages to mapped chats // PrepareMessageContent creates a simple text message
func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string) client.InputMessageContent { func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageContent {
return c.prepareOutgoingMessageContent(text, nil)
}
// ProcessOutgoingMessage executes commands or sends messages to mapped chats, returns message id
func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64) int64 {
if !c.Online() { if !c.Online() {
// we're offline // we're offline
return nil return 0
} }
if returnJid != "" && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) {
// try to execute commands // try to execute commands
response, isCommand := c.ProcessChatCommand(chatID, text) response, isCommand := c.ProcessChatCommand(chatID, text)
if response != "" { if response != "" {
gateway.SendMessage(returnJid, strconv.FormatInt(chatID, 10), response, c.xmpp) c.returnMessage(returnJid, chatID, response)
} }
// do not send on success // do not send on success
if isCommand { if isCommand {
return nil return 0
} }
} }
@ -821,67 +1045,41 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
// quotations // quotations
var reply int64 var reply int64
replySlice := replyRegex.FindStringSubmatch(text) if replaceId == 0 && replyId == 0 {
if len(replySlice) > 1 { replySlice := replyRegex.FindStringSubmatch(text)
reply, _ = strconv.ParseInt(replySlice[1], 10, 64) if len(replySlice) > 1 {
reply, _ = strconv.ParseInt(replySlice[1], 10, 64)
}
} else {
reply = replyId
} }
// attach a file // attach a file
var file *client.InputFileLocal var file *client.InputFileLocal
if chatID != 0 && c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { if c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) {
response, err := http.Get(text) response, err := http.Get(text)
if err != nil { if err != nil {
gateway.SendMessage( c.returnError(returnJid, chatID, "Failed to fetch the uploaded file", err)
returnJid,
strconv.FormatInt(chatID, 10),
fmt.Sprintf("Failed to fetch the uploaded file: %s", err.Error()),
c.xmpp,
)
return nil
} }
if response != nil && response.Body != nil { if response != nil && response.Body != nil {
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode != 200 { if response.StatusCode != 200 {
gateway.SendMessage( c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode))
returnJid,
strconv.FormatInt(chatID, 10),
fmt.Sprintf("Received status code %v", response.StatusCode),
c.xmpp,
)
return nil
} }
tempDir, err := ioutil.TempDir("", "telegabber-*") tempDir, err := ioutil.TempDir("", "telegabber-*")
if err != nil { if err != nil {
gateway.SendMessage( c.returnError(returnJid, chatID, "Failed to create a temporary directory", err)
returnJid,
strconv.FormatInt(chatID, 10),
fmt.Sprintf("Failed to create a temporary directory: %s", err.Error()),
c.xmpp,
)
return nil
} }
tempFile, err := os.Create(filepath.Join(tempDir, filepath.Base(text))) tempFile, err := os.Create(filepath.Join(tempDir, filepath.Base(text)))
if err != nil { if err != nil {
gateway.SendMessage( c.returnError(returnJid, chatID, "Failed to create a temporary file", err)
returnJid,
strconv.FormatInt(chatID, 10),
fmt.Sprintf("Failed to create a temporary file: %s", err.Error()),
c.xmpp,
)
return nil
} }
_, err = io.Copy(tempFile, response.Body) _, err = io.Copy(tempFile, response.Body)
if err != nil { if err != nil {
gateway.SendMessage( c.returnError(returnJid, chatID, "Failed to write a temporary file", err)
returnJid,
strconv.FormatInt(chatID, 10),
fmt.Sprintf("Failed to write a temporary file: %s", err.Error()),
c.xmpp,
)
return nil
} }
file = &client.InputFileLocal{ file = &client.InputFileLocal{
@ -891,7 +1089,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
} }
// remove first line from text // remove first line from text
if file != nil || reply != 0 { if file != nil || (reply != 0 && replyId == 0) {
newlinePos := strings.Index(text, newlineChar) newlinePos := strings.Index(text, newlineChar)
if newlinePos != -1 { if newlinePos != -1 {
text = text[newlinePos+1:] text = text[newlinePos+1:]
@ -900,42 +1098,60 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
} }
} }
content := c.prepareOutgoingMessageContent(text, file)
if replaceId != 0 {
tgMessage, err := c.client.EditMessageText(&client.EditMessageTextRequest{
ChatId: chatID,
MessageId: replaceId,
InputMessageContent: content,
})
if err != nil {
c.returnError(returnJid, chatID, "Not edited", err)
return 0
}
return tgMessage.Id
}
tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{
ChatId: chatID,
ReplyToMessageId: reply,
InputMessageContent: content,
})
if err != nil {
c.returnError(returnJid, chatID, "Not sent", err)
return 0
}
return tgMessage.Id
}
func (c *Client) returnMessage(returnJid string, chatID int64, text string) {
gateway.SendTextMessage(returnJid, strconv.FormatInt(chatID, 10), text, c.xmpp)
}
func (c *Client) returnError(returnJid string, chatID int64, msg string, err error) {
c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error()))
}
func (c *Client) prepareOutgoingMessageContent(text string, file *client.InputFileLocal) client.InputMessageContent {
formattedText := &client.FormattedText{ formattedText := &client.FormattedText{
Text: text, Text: text,
} }
var message client.InputMessageContent var content client.InputMessageContent
if file != nil { if file != nil {
// we can try to send a document // we can try to send a document
message = &client.InputMessageDocument{ content = &client.InputMessageDocument{
Document: file, Document: file,
Caption: formattedText, Caption: formattedText,
} }
} else { } else {
// compile our message // compile our message
message = &client.InputMessageText{ content = &client.InputMessageText{
Text: formattedText, Text: formattedText,
} }
} }
return content
if chatID != 0 {
_, err := c.client.SendMessage(&client.SendMessageRequest{
ChatId: chatID,
ReplyToMessageId: reply,
InputMessageContent: message,
})
if err != nil {
gateway.SendMessage(
returnJid,
strconv.FormatInt(chatID, 10),
fmt.Sprintf("Not sent: %s", err.Error()),
c.xmpp,
)
}
return nil
} else {
return message
}
} }
// StatusesRange proxies the following function from unexported cache // StatusesRange proxies the following function from unexported cache
@ -962,11 +1178,33 @@ func (c *Client) deleteResource(resource string) {
} }
} }
func (c *Client) resourcesRange() chan string {
c.locks.resourcesLock.Lock()
resourceChan := make(chan string, 1)
go func() {
defer func() {
c.locks.resourcesLock.Unlock()
close(resourceChan)
}()
for resource := range c.resources {
resourceChan <- resource
}
}()
return resourceChan
}
// resend statuses to (to another resource, for example) // resend statuses to (to another resource, for example)
func (c *Client) roster(resource string) { func (c *Client) roster(resource string) {
c.locks.resourcesLock.Lock()
if _, ok := c.resources[resource]; ok { if _, ok := c.resources[resource]; ok {
c.locks.resourcesLock.Unlock()
return // we know it return // we know it
} }
c.locks.resourcesLock.Unlock()
log.Warnf("Sending roster for %v", resource) log.Warnf("Sending roster for %v", resource)
@ -980,7 +1218,7 @@ func (c *Client) roster(resource string) {
} }
// get last messages from specified chat // get last messages from specified chat
func (c *Client) getLastMessages(id int64, query string, from int64, count int32) (*client.Messages, error) { func (c *Client) getLastMessages(id int64, query string, from int64, count int32) (*client.FoundChatMessages, error) {
return c.client.SearchChatMessages(&client.SearchChatMessagesRequest{ return c.client.SearchChatMessages(&client.SearchChatMessagesRequest{
ChatId: id, ChatId: id,
Query: query, Query: query,
@ -999,20 +1237,20 @@ func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*clie
}) })
} }
// OpenPhotoFile reliably obtains a photo if possible // ForceOpenFile reliably obtains a file if possible
func (c *Client) OpenPhotoFile(photoFile *client.File, priority int32) (*os.File, string, error) { func (c *Client) ForceOpenFile(tgFile *client.File, priority int32) (*os.File, string, error) {
if photoFile == nil { if tgFile == nil {
return nil, "", errors.New("Photo file not found") return nil, "", errors.New("File not found")
} }
path := photoFile.Local.Path path := tgFile.Local.Path
file, err := os.Open(path) file, err := os.Open(path)
if err == nil { if err == nil {
return file, path, nil return file, path, nil
} else } else
// obtain the photo right now if still not downloaded // obtain the photo right now if still not downloaded
if !photoFile.Local.IsDownloadingCompleted { if !tgFile.Local.IsDownloadingCompleted {
tdFile, tdErr := c.DownloadFile(photoFile.Id, priority, true) tdFile, tdErr := c.DownloadFile(tgFile.Id, priority, true)
if tdErr == nil { if tdErr == nil {
path = tdFile.Local.Path path = tdFile.Local.Path
file, err = os.Open(path) file, err = os.Open(path)
@ -1033,10 +1271,18 @@ func (c *Client) GetChatDescription(chat *client.Chat) string {
UserId: privateType.UserId, UserId: privateType.UserId,
}) })
if err == nil { if err == nil {
if fullInfo.Bio != "" { if fullInfo.Bio != nil && fullInfo.Bio.Text != "" {
return fullInfo.Bio return formatter.Format(
} else if fullInfo.Description != "" { fullInfo.Bio.Text,
return fullInfo.Description fullInfo.Bio.Entities,
c.getFormatter(),
)
} else if fullInfo.BotInfo != nil {
if fullInfo.BotInfo.ShortDescription != "" {
return fullInfo.BotInfo.ShortDescription
} else {
return fullInfo.BotInfo.Description
}
} }
} else { } else {
log.Warnf("Couldn't retrieve private chat info: %v", err.Error()) log.Warnf("Couldn't retrieve private chat info: %v", err.Error())
@ -1155,3 +1401,162 @@ func (c *Client) prepareDiskSpace(size uint64) {
} }
} }
} }
func (c *Client) GetVcardInfo(toID int64) (VCardInfo, error) {
var info VCardInfo
chat, user, err := c.GetContactByID(toID, nil)
if err != nil {
return info, err
}
if chat != nil {
info.Fn = chat.Title
if chat.Photo != nil {
info.Photo = chat.Photo.Small
}
info.Info = c.GetChatDescription(chat)
}
if user != nil {
if user.Usernames != nil {
info.Nicknames = make([]string, len(user.Usernames.ActiveUsernames))
copy(info.Nicknames, user.Usernames.ActiveUsernames)
}
info.Given = user.FirstName
info.Family = user.LastName
info.Tel = user.PhoneNumber
}
return info, nil
}
func (c *Client) UpdateChatNicknames() {
for _, id := range c.cache.ChatsKeys() {
chat, ok := c.cache.GetChat(id)
if ok {
newArgs := []args.V{
gateway.SPFrom(strconv.FormatInt(id, 10)),
gateway.SPNickname(chat.Title),
}
cachedStatus, ok := c.cache.GetStatus(id)
if ok {
show, status, typ := cachedStatus.Destruct()
newArgs = append(newArgs, gateway.SPShow(show), gateway.SPStatus(status))
if typ != "" {
newArgs = append(newArgs, gateway.SPType(typ))
}
}
gateway.SendPresence(
c.xmpp,
c.jid,
newArgs...,
)
gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp)
}
}
}
// AddToOutbox remembers the resource from which a message with given ID was sent
func (c *Client) AddToOutbox(xmppId, resource string) {
c.locks.outboxLock.Lock()
defer c.locks.outboxLock.Unlock()
c.outbox[xmppId] = resource
}
func (c *Client) popFromOutbox(xmppId string) string {
c.locks.outboxLock.Lock()
defer c.locks.outboxLock.Unlock()
resource, ok := c.outbox[xmppId]
if ok {
delete(c.outbox, xmppId)
} else {
log.Warnf("No %v xmppId in outbox", xmppId)
}
return resource
}
func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []string {
var jids []string
if isOutgoing {
for resource := range c.resourcesRange() {
if ignoredResource == "" || resource != ignoredResource {
jids = append(jids, c.jid+"/"+resource)
}
}
} else {
jids = []string{c.jid}
}
return jids
}
func (c *Client) calculateMessageHash(messageId int64, content client.MessageContent) uint64 {
var h maphash.Hash
h.SetSeed(c.msgHashSeed)
buf8 := make([]byte, 8)
binary.BigEndian.PutUint64(buf8, uint64(messageId))
h.Write(buf8)
if content != nil && content.MessageContentType() == client.TypeMessageText {
textContent, ok := content.(*client.MessageText)
if !ok {
uhOh()
}
if textContent.Text != nil {
h.WriteString(textContent.Text.Text)
for _, entity := range textContent.Text.Entities {
buf4 := make([]byte, 4)
binary.BigEndian.PutUint32(buf4, uint32(entity.Offset))
h.Write(buf4)
binary.BigEndian.PutUint32(buf4, uint32(entity.Length))
h.Write(buf4)
h.WriteString(entity.Type.TextEntityTypeType())
}
}
}
return h.Sum64()
}
func (c *Client) updateLastMessageHash(chatId, messageId int64, content client.MessageContent) {
c.locks.lastMsgHashesLock.Lock()
defer c.locks.lastMsgHashesLock.Unlock()
c.lastMsgHashes[chatId] = c.calculateMessageHash(messageId, content)
}
func (c *Client) hasLastMessageHashChanged(chatId, messageId int64, content client.MessageContent) bool {
c.locks.lastMsgHashesLock.Lock()
defer c.locks.lastMsgHashesLock.Unlock()
oldHash, ok := c.lastMsgHashes[chatId]
newHash := c.calculateMessageHash(messageId, content)
if !ok {
log.Warnf("Last message hash for chat %v does not exist", chatId)
}
log.WithFields(log.Fields{
"old hash": oldHash,
"new hash": newHash,
}).Info("Message hashes")
return !ok || oldHash != newHash
}
func (c *Client) getFormatter() func(*client.TextEntity) (*formatter.Insertion, *formatter.Insertion) {
return formatter.EntityToXEP0393
}
func (c *Client) usernamesToString(usernames []string) string {
var atUsernames []string
for _, username := range usernames {
atUsernames = append(atUsernames, "@"+username)
}
return strings.Join(atUsernames, ", ")
}

View file

@ -146,10 +146,13 @@ func TestFormatFile(t *testing.T) {
}, },
} }
content := c.formatFile(&file, false) content, link := c.formatFile(&file, false)
if content != ". (23 kbytes) | " { if content != ". (23 kbytes) | " {
t.Errorf("Wrong file label: %v", content) t.Errorf("Wrong file label: %v", content)
} }
if link != "" {
t.Errorf("Wrong file link: %v", link)
}
} }
func TestFormatPreview(t *testing.T) { func TestFormatPreview(t *testing.T) {
@ -168,10 +171,13 @@ func TestFormatPreview(t *testing.T) {
}, },
} }
content := c.formatFile(&file, true) content, link := c.formatFile(&file, true)
if content != "" { if content != "" {
t.Errorf("Wrong preview label: %v", content) t.Errorf("Wrong preview label: %v", content)
} }
if link != "" {
t.Errorf("Wrong preview link: %v", link)
}
} }
func TestMessageToTextSticker(t *testing.T) { func TestMessageToTextSticker(t *testing.T) {
@ -363,6 +369,53 @@ func TestMessageAnimation(t *testing.T) {
} }
} }
func TestMessageTtl1(t *testing.T) {
ttl := client.Message{
Content: &client.MessageChatSetMessageAutoDeleteTime{},
}
text := (&Client{}).messageToText(&ttl, false)
if text != "The self-destruct timer was disabled" {
t.Errorf("Wrong anonymous off ttl label: %v", text)
}
}
func TestMessageTtl2(t *testing.T) {
ttl := client.Message{
Content: &client.MessageChatSetMessageAutoDeleteTime{
MessageAutoDeleteTime: 3,
},
}
text := (&Client{}).messageToText(&ttl, false)
if text != "The self-destruct timer was set to 3 seconds" {
t.Errorf("Wrong anonymous ttl label: %v", text)
}
}
func TestMessageTtl3(t *testing.T) {
ttl := client.Message{
Content: &client.MessageChatSetMessageAutoDeleteTime{
FromUserId: 3,
},
}
text := (&Client{}).messageToText(&ttl, false)
if text != "unknown contact: TDlib instance is offline disabled the self-destruct timer" {
t.Errorf("Wrong off ttl label: %v", text)
}
}
func TestMessageTtl4(t *testing.T) {
ttl := client.Message{
Content: &client.MessageChatSetMessageAutoDeleteTime{
FromUserId: 3,
MessageAutoDeleteTime: 3,
},
}
text := (&Client{}).messageToText(&ttl, false)
if text != "unknown contact: TDlib instance is offline set the self-destruct timer to 3 seconds" {
t.Errorf("Wrong ttl label: %v", text)
}
}
func TestMessageUnknown(t *testing.T) { func TestMessageUnknown(t *testing.T) {
unknown := client.Message{ unknown := client.Message{
Content: &client.MessageExpiredPhoto{}, Content: &client.MessageExpiredPhoto{},
@ -383,10 +436,16 @@ func TestMessageToPrefix1(t *testing.T) {
}, },
}, },
} }
prefix := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "") prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", nil)
if prefix != "➡ 42 | fwd: ziz" { if prefix != "➡ 42 | fwd: ziz" {
t.Errorf("Wrong prefix: %v", prefix) t.Errorf("Wrong prefix: %v", prefix)
} }
if replyStart != 0 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 0 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
} }
func TestMessageToPrefix2(t *testing.T) { func TestMessageToPrefix2(t *testing.T) {
@ -398,10 +457,16 @@ func TestMessageToPrefix2(t *testing.T) {
}, },
}, },
} }
prefix := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "") prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", nil)
if prefix != "⬅ 56 | fwd: (zaz) | preview: y.jpg" { if prefix != "⬅ 56 | fwd: (zaz) | preview: y.jpg" {
t.Errorf("Wrong prefix: %v", prefix) t.Errorf("Wrong prefix: %v", prefix)
} }
if replyStart != 0 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 0 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
} }
func TestMessageToPrefix3(t *testing.T) { func TestMessageToPrefix3(t *testing.T) {
@ -413,10 +478,16 @@ func TestMessageToPrefix3(t *testing.T) {
}, },
}, },
} }
prefix := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg") prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg", nil)
if prefix != "< 56 | fwd: (zuz) | file: a.jpg" { if prefix != "< 56 | fwd: (zuz) | file: a.jpg" {
t.Errorf("Wrong prefix: %v", prefix) t.Errorf("Wrong prefix: %v", prefix)
} }
if replyStart != 0 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 0 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
} }
func TestMessageToPrefix4(t *testing.T) { func TestMessageToPrefix4(t *testing.T) {
@ -424,10 +495,16 @@ func TestMessageToPrefix4(t *testing.T) {
Id: 23, Id: 23,
IsOutgoing: true, IsOutgoing: true,
} }
prefix := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "") prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", nil)
if prefix != "> 23" { if prefix != "> 23" {
t.Errorf("Wrong prefix: %v", prefix) t.Errorf("Wrong prefix: %v", prefix)
} }
if replyStart != 0 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 0 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
} }
func TestMessageToPrefix5(t *testing.T) { func TestMessageToPrefix5(t *testing.T) {
@ -439,8 +516,72 @@ func TestMessageToPrefix5(t *testing.T) {
}, },
}, },
} }
prefix := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg") prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg", nil)
if prefix != "< 560 | fwd: (zyz) | preview: h.jpg | file: a.jpg" { if prefix != "< 560 | fwd: (zyz) | preview: h.jpg | file: a.jpg" {
t.Errorf("Wrong prefix: %v", prefix) t.Errorf("Wrong prefix: %v", prefix)
} }
if replyStart != 0 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 0 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
}
func TestMessageToPrefix6(t *testing.T) {
message := client.Message{
Id: 23,
IsOutgoing: true,
ReplyToMessageId: 42,
}
reply := client.Message{
Id: 42,
Content: &client.MessageText{
Text: &client.FormattedText{
Text: "tist",
},
},
}
prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", &reply)
if prefix != "> 23 | reply: 42 | | tist" {
t.Errorf("Wrong prefix: %v", prefix)
}
if replyStart != 4 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 26 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
}
func GetSenderIdEmpty(t *testing.T) {
message := client.Message{}
senderId := (&Client{}).getSenderId(&message)
if senderId != 0 {
t.Errorf("Wrong sender id: %v", senderId)
}
}
func GetSenderIdUser(t *testing.T) {
message := client.Message{
SenderId: &client.MessageSenderUser{
UserId: 42,
},
}
senderId := (&Client{}).getSenderId(&message)
if senderId != 42 {
t.Errorf("Wrong sender id: %v", senderId)
}
}
func GetSenderIdChat(t *testing.T) {
message := client.Message{
SenderId: &client.MessageSenderChat{
ChatId: -42,
},
}
senderId := (&Client{}).getSenderId(&message)
if senderId != -42 {
t.Errorf("Wrong sender id: %v", senderId)
}
} }

View file

@ -7,6 +7,7 @@ import (
"sync" "sync"
"time" "time"
"dev.narayana.im/narayana/telegabber/badger"
"dev.narayana.im/narayana/telegabber/config" "dev.narayana.im/narayana/telegabber/config"
"dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/persistence"
"dev.narayana.im/narayana/telegabber/telegram" "dev.narayana.im/narayana/telegabber/telegram"
@ -38,7 +39,7 @@ var sizeRegex = regexp.MustCompile("\\A([0-9]+) ?([KMGTPE]?B?)\\z")
// NewComponent starts a new component and wraps it in // NewComponent starts a new component and wraps it in
// a stream manager that you should start yourself // a stream manager that you should start yourself
func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig) (*xmpp.StreamManager, *xmpp.Component, error) { func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig, idsPath string) (*xmpp.StreamManager, *xmpp.Component, error) {
var err error var err error
gateway.Jid, err = stanza.NewJid(conf.Jid) gateway.Jid, err = stanza.NewJid(conf.Jid)
@ -53,6 +54,8 @@ func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig) (*xmpp.Strea
} }
} }
gateway.IdsDB = badger.IdsDBOpen(idsPath)
tgConf = tc tgConf = tc
if tc.Content.Quota != "" { if tc.Content.Quota != "" {
@ -163,6 +166,8 @@ func heartbeat(component *xmpp.Component) {
// it would be resolved on the next iteration // it would be resolved on the next iteration
SaveSessions() SaveSessions()
} }
gateway.IdsDB.Gc()
} }
} }
@ -240,6 +245,9 @@ func Close(component *xmpp.Component) {
// save sessions // save sessions
SaveSessions() SaveSessions()
// flush the ids database
gateway.IdsDB.Close()
// close stream // close stream
component.Disconnect() component.Disconnect()
} }

View file

@ -2,6 +2,7 @@ package extensions
import ( import (
"encoding/xml" "encoding/xml"
"strconv"
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
@ -111,6 +112,107 @@ type IqVcardDesc struct {
Text string `xml:",chardata"` Text string `xml:",chardata"`
} }
// Reply is from XEP-0461
type Reply struct {
XMLName xml.Name `xml:"urn:xmpp:reply:0 reply"`
To string `xml:"to,attr"`
Id string `xml:"id,attr"`
}
// Fallback is from XEP-0428
type Fallback struct {
XMLName xml.Name `xml:"urn:xmpp:fallback:0 fallback"`
For string `xml:"for,attr"`
Body []FallbackBody `xml:"urn:xmpp:fallback:0 body"`
Subject []FallbackSubject `xml:"urn:xmpp:fallback:0 subject"`
}
// FallbackBody is from XEP-0428
type FallbackBody struct {
XMLName xml.Name `xml:"urn:xmpp:fallback:0 body"`
Start string `xml:"start,attr"`
End string `xml:"end,attr"`
}
// FallbackSubject is from XEP-0428
type FallbackSubject struct {
XMLName xml.Name `xml:"urn:xmpp:fallback:0 subject"`
Start string `xml:"start,attr"`
End string `xml:"end,attr"`
}
// CarbonReceived is from XEP-0280
type CarbonReceived struct {
XMLName xml.Name `xml:"urn:xmpp:carbons:2 received"`
Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"`
}
// CarbonSent is from XEP-0280
type CarbonSent struct {
XMLName xml.Name `xml:"urn:xmpp:carbons:2 sent"`
Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"`
}
// ComponentPrivilege is from XEP-0356
type ComponentPrivilege1 struct {
XMLName xml.Name `xml:"urn:xmpp:privilege:1 privilege"`
Perms []ComponentPerm `xml:"perm"`
Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"`
}
// ComponentPrivilege is from XEP-0356
type ComponentPrivilege2 struct {
XMLName xml.Name `xml:"urn:xmpp:privilege:2 privilege"`
Perms []ComponentPerm `xml:"perm"`
Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"`
}
// ComponentPerm is from XEP-0356
type ComponentPerm struct {
XMLName xml.Name `xml:"perm"`
Access string `xml:"access,attr"`
Type string `xml:"type,attr"`
Push bool `xml:"push,attr"`
}
// ClientMessage is a jabber:client NS message compatible with Prosody's XEP-0356 implementation
type ClientMessage struct {
XMLName xml.Name `xml:"jabber:client message"`
stanza.Attrs
Subject string `xml:"subject,omitempty"`
Body string `xml:"body,omitempty"`
Thread string `xml:"thread,omitempty"`
Error stanza.Err `xml:"error,omitempty"`
Extensions []stanza.MsgExtension `xml:",omitempty"`
}
// Replace is from XEP-0308
type Replace struct {
XMLName xml.Name `xml:"urn:xmpp:message-correct:0 replace"`
Id string `xml:"id,attr"`
}
// QueryRegister is from XEP-0077
type QueryRegister struct {
XMLName xml.Name `xml:"jabber:iq:register query"`
Instructions string `xml:"instructions"`
Username string `xml:"username"`
Registered *QueryRegisterRegistered `xml:"registered"`
Remove *QueryRegisterRemove `xml:"remove"`
ResultSet *stanza.ResultSet `xml:"set,omitempty"`
}
// QueryRegisterRegistered is a child element from XEP-0077
type QueryRegisterRegistered struct {
XMLName xml.Name `xml:"registered"`
}
// QueryRegisterRemove is a child element from XEP-0077
type QueryRegisterRemove struct {
XMLName xml.Name `xml:"remove"`
}
// Namespace is a namespace! // Namespace is a namespace!
func (c PresenceNickExtension) Namespace() string { func (c PresenceNickExtension) Namespace() string {
return c.XMLName.Space return c.XMLName.Space
@ -131,6 +233,69 @@ func (c IqVcardTemp) GetSet() *stanza.ResultSet {
return c.ResultSet return c.ResultSet
} }
// Namespace is a namespace!
func (c Reply) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c Fallback) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c CarbonReceived) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c CarbonSent) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c ComponentPrivilege1) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c ComponentPrivilege2) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c Replace) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c QueryRegister) Namespace() string {
return c.XMLName.Space
}
// GetSet getsets!
func (c QueryRegister) GetSet() *stanza.ResultSet {
return c.ResultSet
}
// Name is a packet name
func (ClientMessage) Name() string {
return "message"
}
// NewReplyFallback initializes a fallback range
func NewReplyFallback(start uint64, end uint64) Fallback {
return Fallback{
For: "urn:xmpp:reply:0",
Body: []FallbackBody{
FallbackBody{
Start: strconv.FormatUint(start, 10),
End: strconv.FormatUint(end, 10),
},
},
}
}
func init() { func init() {
// presence nick // presence nick
stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{ stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{
@ -149,4 +314,52 @@ func init() {
"vcard-temp", "vcard-temp",
"vCard", "vCard",
}, IqVcardTemp{}) }, IqVcardTemp{})
// reply
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:reply:0",
"reply",
}, Reply{})
// fallback
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:fallback:0",
"fallback",
}, Fallback{})
// carbon received
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:carbons:2",
"received",
}, CarbonReceived{})
// carbon sent
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:carbons:2",
"sent",
}, CarbonSent{})
// component privilege v1
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:privilege:1",
"privilege",
}, ComponentPrivilege1{})
// component privilege v2
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:privilege:2",
"privilege",
}, ComponentPrivilege2{})
// message edit
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:message-correct:0",
"replace",
}, Replace{})
// register query
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{
"jabber:iq:register",
"query",
}, QueryRegister{})
} }

View file

@ -2,9 +2,11 @@ package gateway
import ( import (
"encoding/xml" "encoding/xml"
"github.com/pkg/errors"
"strings" "strings"
"sync" "sync"
"dev.narayana.im/narayana/telegabber/badger"
"dev.narayana.im/narayana/telegabber/xmpp/extensions" "dev.narayana.im/narayana/telegabber/xmpp/extensions"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -13,6 +15,13 @@ import (
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
type Reply struct {
Author string
Id string
Start uint64
End uint64
}
const NSNick string = "http://jabber.org/protocol/nick" const NSNick string = "http://jabber.org/protocol/nick"
// Queue stores presences to send later // Queue stores presences to send later
@ -22,16 +31,51 @@ var QueueLock = sync.Mutex{}
// Jid stores the component's JID object // Jid stores the component's JID object
var Jid *stanza.Jid var Jid *stanza.Jid
// IdsDB provides a disk-backed bidirectional dictionary of Telegram and XMPP ids
var IdsDB badger.IdsDB
// DirtySessions denotes that some Telegram session configurations // DirtySessions denotes that some Telegram session configurations
// were changed and need to be re-flushed to the YamlDB // were changed and need to be re-flushed to the YamlDB
var DirtySessions = false var DirtySessions = false
// MessageOutgoingPermissionVersion contains a XEP-0356 version to fake outgoing messages by foreign JIDs
var MessageOutgoingPermissionVersion = 0
// SendMessage creates and sends a message stanza // SendMessage creates and sends a message stanza
func SendMessage(to string, from string, body string, component *xmpp.Component) { func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isCarbon bool) {
sendMessageWrapper(to, from, body, id, component, reply, "", isCarbon)
}
// SendServiceMessage creates and sends a simple message stanza from transport
func SendServiceMessage(to string, body string, component *xmpp.Component) {
sendMessageWrapper(to, "", body, "", component, nil, "", false)
}
// SendTextMessage creates and sends a simple message stanza
func SendTextMessage(to string, from string, body string, component *xmpp.Component) {
sendMessageWrapper(to, from, body, "", component, nil, "", false)
}
// SendMessageWithOOB creates and sends a message stanza with OOB URL
func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) {
sendMessageWrapper(to, from, body, id, component, reply, oob, isCarbon)
}
func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) {
toJid, err := stanza.NewJid(to)
if err != nil {
log.WithFields(log.Fields{
"to": to,
}).Error(errors.Wrap(err, "Invalid to JID!"))
return
}
bareTo := toJid.Bare()
componentJid := Jid.Full() componentJid := Jid.Full()
var logFrom string var logFrom string
var messageFrom string var messageFrom string
var messageTo string
if from == "" { if from == "" {
logFrom = componentJid logFrom = componentJid
messageFrom = componentJid messageFrom = componentJid
@ -39,6 +83,12 @@ func SendMessage(to string, from string, body string, component *xmpp.Component)
logFrom = from logFrom = from
messageFrom = from + "@" + componentJid messageFrom = from + "@" + componentJid
} }
if isCarbon {
messageTo = messageFrom
messageFrom = bareTo + "/" + Jid.Resource
} else {
messageTo = to
}
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"from": logFrom, "from": logFrom,
@ -48,13 +98,67 @@ func SendMessage(to string, from string, body string, component *xmpp.Component)
message := stanza.Message{ message := stanza.Message{
Attrs: stanza.Attrs{ Attrs: stanza.Attrs{
From: messageFrom, From: messageFrom,
To: to, To: messageTo,
Type: "chat", Type: "chat",
Id: id,
}, },
Body: body, Body: body,
} }
sendMessage(&message, component) if oob != "" {
message.Extensions = append(message.Extensions, stanza.OOB{
URL: oob,
})
}
if reply != nil {
message.Extensions = append(message.Extensions, extensions.Reply{
To: reply.Author,
Id: reply.Id,
})
if reply.End > 0 {
message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End))
}
}
if !isCarbon && toJid.Resource != "" {
message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
}
if isCarbon {
carbonMessage := extensions.ClientMessage{
Attrs: stanza.Attrs{
From: bareTo,
To: to,
Type: "chat",
},
}
carbonMessage.Extensions = append(carbonMessage.Extensions, extensions.CarbonSent{
Forwarded: stanza.Forwarded{
Stanza: extensions.ClientMessage(message),
},
})
privilegeMessage := stanza.Message{
Attrs: stanza.Attrs{
From: Jid.Bare(),
To: toJid.Domain,
},
}
if MessageOutgoingPermissionVersion == 2 {
privilegeMessage.Extensions = append(privilegeMessage.Extensions, extensions.ComponentPrivilege2{
Forwarded: stanza.Forwarded{
Stanza: carbonMessage,
},
})
} else {
privilegeMessage.Extensions = append(privilegeMessage.Extensions, extensions.ComponentPrivilege1{
Forwarded: stanza.Forwarded{
Stanza: carbonMessage,
},
})
}
sendMessage(&privilegeMessage, component)
} else {
sendMessage(&message, component)
}
} }
// SetNickname sets a new nickname for a contact // SetNickname sets a new nickname for a contact
@ -255,3 +359,21 @@ func ResumableSend(component *xmpp.Component, packet stanza.Packet) error {
} }
return err return err
} }
// SubscribeToTransport ensures a two-way subscription to the transport
func SubscribeToTransport(component *xmpp.Component, jid string) {
SendPresence(component, jid, SPType("subscribe"))
SendPresence(component, jid, SPType("subscribed"))
}
// SplitJID tokenizes a JID string to bare JID and resource
func SplitJID(from string) (string, string, bool) {
fromJid, err := stanza.NewJid(from)
if err != nil {
log.WithFields(log.Fields{
"from": from,
}).Error(errors.Wrap(err, "Invalid from JID!"))
return "", "", false
}
return fromJid.Bare(), fromJid.Resource, true
}

View file

@ -4,16 +4,19 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/xml" "encoding/xml"
"fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
"io" "io"
"strconv" "strconv"
"strings" "strings"
"dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/persistence"
"dev.narayana.im/narayana/telegabber/telegram"
"dev.narayana.im/narayana/telegabber/xmpp/extensions" "dev.narayana.im/narayana/telegabber/xmpp/extensions"
"dev.narayana.im/narayana/telegabber/xmpp/gateway" "dev.narayana.im/narayana/telegabber/xmpp/gateway"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/soheilhy/args"
"gosrc.io/xmpp" "gosrc.io/xmpp"
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
@ -66,6 +69,17 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
go handleGetDisco(discoTypeItems, s, iq) go handleGetDisco(discoTypeItems, s, iq)
return return
} }
_, ok = iq.Payload.(*extensions.QueryRegister)
if ok {
go handleGetQueryRegister(s, iq)
return
}
} else if iq.Type == "set" {
query, ok := iq.Payload.(*extensions.QueryRegister)
if ok {
go handleSetQueryRegister(s, iq, query)
return
}
} }
} }
@ -90,7 +104,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
}).Warn("Message") }).Warn("Message")
log.Debugf("%#v", msg) log.Debugf("%#v", msg)
bare, resource, ok := splitFrom(msg.From) bare, resource, ok := gateway.SplitJID(msg.From)
if !ok { if !ok {
return return
} }
@ -100,30 +114,164 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
session, ok := sessions[bare] session, ok := sessions[bare]
if !ok { if !ok {
if msg.To == gatewayJid { if msg.To == gatewayJid {
gateway.SendPresence(component, msg.From, gateway.SPType("subscribe")) gateway.SubscribeToTransport(component, msg.From)
gateway.SendPresence(component, msg.From, gateway.SPType("subscribed"))
} else { } else {
log.Error("Message from stranger") log.Error("Message from stranger")
return
} }
return
} }
toID, ok := toToID(msg.To) toID, ok := toToID(msg.To)
if ok { if ok {
session.ProcessOutgoingMessage(toID, msg.Body, msg.From) var reply extensions.Reply
var fallback extensions.Fallback
var replace extensions.Replace
msg.Get(&reply)
msg.Get(&fallback)
msg.Get(&replace)
log.Debugf("reply: %#v", reply)
log.Debugf("fallback: %#v", fallback)
log.Debugf("replace: %#v", replace)
var replyId int64
var err error
text := msg.Body
if len(reply.Id) > 0 {
chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, reply.Id)
if err == nil {
if chatId != toID {
log.Warnf("Chat mismatch: %v ≠ %v", chatId, toID)
} else {
replyId = msgId
log.Debugf("replace tg: %#v %#v", chatId, msgId)
}
} else {
id := reply.Id
if id[0] == 'e' {
id = id[1:]
}
replyId, err = strconv.ParseInt(id, 10, 64)
if err != nil {
log.Warn(errors.Wrap(err, "Failed to parse message ID!"))
}
}
if replyId != 0 && fallback.For == "urn:xmpp:reply:0" && len(fallback.Body) > 0 {
body := fallback.Body[0]
var start, end int64
start, err = strconv.ParseInt(body.Start, 10, 64)
if err != nil {
log.WithFields(log.Fields{
"start": body.Start,
}).Warn(errors.Wrap(err, "Failed to parse fallback start!"))
}
end, err = strconv.ParseInt(body.End, 10, 64)
if err != nil {
log.WithFields(log.Fields{
"end": body.End,
}).Warn(errors.Wrap(err, "Failed to parse fallback end!"))
}
fullRunes := []rune(text)
cutRunes := make([]rune, 0, len(text)-int(end-start))
cutRunes = append(cutRunes, fullRunes[:start]...)
cutRunes = append(cutRunes, fullRunes[end:]...)
text = string(cutRunes)
}
}
var replaceId int64
if replace.Id != "" {
chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, replace.Id)
if err == nil {
if chatId != toID {
gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "<ERROR: Chat mismatch>", component)
return
}
replaceId = msgId
log.Debugf("replace tg: %#v %#v", chatId, msgId)
} else {
gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "<ERROR: Could not find matching message to edit>", component)
return
}
}
session.SendMessageLock.Lock()
defer session.SendMessageLock.Unlock()
tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId)
if tgMessageId != 0 {
if replaceId != 0 {
// not needed (is it persistent among clients though?)
/* err = gateway.IdsDB.ReplaceIdPair(session.Session.Login, bare, replace.Id, msg.Id, tgMessageId)
if err != nil {
log.Errorf("Failed to replace id %v with %v %v", replace.Id, msg.Id, tgMessageId)
} */
session.AddToOutbox(replace.Id, resource)
} else {
err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id)
if err != nil {
log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id)
}
}
} else {
/*
// if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway
if replaceId != 0 {
err = gateway.IdsDB.ReplaceXmppId(session.Session.Login, bare, replace.Id, msg.Id)
if err != nil {
log.Errorf("Failed to replace id %v with %v", replace.Id, msg.Id)
}
} */
}
return return
} else { } else {
toJid, err := stanza.NewJid(msg.To) toJid, err := stanza.NewJid(msg.To)
if err == nil && toJid.Bare() == gatewayJid && (strings.HasPrefix(msg.Body, "/") || strings.HasPrefix(msg.Body, "!")) { if err == nil && toJid.Bare() == gatewayJid && (strings.HasPrefix(msg.Body, "/") || strings.HasPrefix(msg.Body, "!")) {
response := session.ProcessTransportCommand(msg.Body, resource) response := session.ProcessTransportCommand(msg.Body, resource)
if response != "" { if response != "" {
gateway.SendMessage(msg.From, "", response, component) gateway.SendServiceMessage(msg.From, response, component)
} }
return return
} }
} }
log.Warn("Unknown purpose of the message, skipping") log.Warn("Unknown purpose of the message, skipping")
} }
if msg.Body == "" {
var privilege1 extensions.ComponentPrivilege1
if ok := msg.Get(&privilege1); ok {
log.Debugf("privilege1: %#v", privilege1)
}
for _, perm := range privilege1.Perms {
if perm.Access == "message" && perm.Type == "outgoing" {
gateway.MessageOutgoingPermissionVersion = 1
}
}
var privilege2 extensions.ComponentPrivilege2
if ok := msg.Get(&privilege2); ok {
log.Debugf("privilege2: %#v", privilege2)
}
for _, perm := range privilege2.Perms {
if perm.Access == "message" && perm.Type == "outgoing" {
gateway.MessageOutgoingPermissionVersion = 2
}
}
}
if msg.Type == "error" {
log.Errorf("MESSAGE ERROR: %#v", p)
if msg.XMLName.Space == "jabber:component:accept" && msg.Error.Code == 401 {
suffix := "@" + msg.From
for bare := range sessions {
if strings.HasSuffix(bare, suffix) {
gateway.SendServiceMessage(bare, "Your server \""+msg.From+"\" does not allow to send carbons", component)
}
}
}
}
} }
// HandlePresence processes an incoming XMPP presence // HandlePresence processes an incoming XMPP presence
@ -168,7 +316,7 @@ func handleSubscription(s xmpp.Sender, p stanza.Presence) {
if !ok { if !ok {
return return
} }
bare, _, ok := splitFrom(p.From) bare, _, ok := gateway.SplitJID(p.From)
if !ok { if !ok {
return return
} }
@ -199,7 +347,7 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) {
log.Debugf("%#v", p) log.Debugf("%#v", p)
// create session // create session
bare, resource, ok := splitFrom(p.From) bare, resource, ok := gateway.SplitJID(p.From)
if !ok { if !ok {
return return
} }
@ -229,13 +377,21 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) {
log.Error(errors.Wrap(err, "TDlib connection failure")) log.Error(errors.Wrap(err, "TDlib connection failure"))
} else { } else {
for status := range session.StatusesRange() { for status := range session.StatusesRange() {
show, description, typ := status.Destruct()
newArgs := []args.V{
gateway.SPImmed(false),
}
if typ != "" {
newArgs = append(newArgs, gateway.SPType(typ))
}
go session.ProcessStatusUpdate( go session.ProcessStatusUpdate(
status.ID, status.ID,
status.Description, description,
status.XMPP, show,
gateway.SPImmed(false), newArgs...,
) )
} }
session.UpdateChatNicknames()
} }
}() }()
} }
@ -265,45 +421,12 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) {
log.Error("Invalid IQ to") log.Error("Invalid IQ to")
return return
} }
chat, user, err := session.GetContactByID(toID, nil) info, err := session.GetVcardInfo(toID)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return return
} }
var fn, photo, nickname, given, family, tel, info string
if chat != nil {
fn = chat.Title
if chat.Photo != nil {
file, path, err := session.OpenPhotoFile(chat.Photo.Small, 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 {
photo = 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())
}
}
info = session.GetChatDescription(chat)
}
if user != nil {
nickname = user.Username
given = user.FirstName
family = user.LastName
tel = user.PhoneNumber
}
answer := stanza.IQ{ answer := stanza.IQ{
Attrs: stanza.Attrs{ Attrs: stanza.Attrs{
From: iq.To, From: iq.To,
@ -311,7 +434,7 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) {
Id: iq.Id, Id: iq.Id,
Type: "result", Type: "result",
}, },
Payload: makeVCardPayload(typ, iq.To, fn, photo, nickname, given, family, tel, info), Payload: makeVCardPayload(typ, iq.To, info, session),
} }
log.Debugf("%#v", answer) log.Debugf("%#v", answer)
@ -340,12 +463,15 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) {
if dt == discoTypeInfo { if dt == discoTypeInfo {
disco := answer.DiscoInfo() disco := answer.DiscoInfo()
toID, toOk := toToID(iq.To) toID, toOk := toToID(iq.To)
if !toOk { if toOk {
disco.AddIdentity("", "account", "registered")
} else {
disco.AddIdentity("Telegram Gateway", "gateway", "telegram") disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
disco.AddFeatures("jabber:iq:register")
} }
var isMuc bool var isMuc bool
bare, _, fromOk := splitFrom(iq.From) bare, _, fromOk := gateway.SplitJID(iq.From)
if fromOk { if fromOk {
session, sessionOk := sessions[bare] session, sessionOk := sessions[bare]
if sessionOk && session.Session.MUC { if sessionOk && session.Session.MUC {
@ -399,7 +525,7 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) {
_, ok := toToID(iq.To) _, ok := toToID(iq.To)
if !ok { if !ok {
bare, _, ok := splitFrom(iq.From) bare, _, ok := gateway.SplitJID(iq.From)
if ok { if ok {
// raw access, no need to create a new instance if not connected // raw access, no need to create a new instance if not connected
session, ok := sessions[bare] session, ok := sessions[bare]
@ -428,15 +554,163 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) {
_ = gateway.ResumableSend(component, answer) _ = gateway.ResumableSend(component, answer)
} }
func splitFrom(from string) (string, string, bool) { func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) {
fromJid, err := stanza.NewJid(from) component, ok := s.(*xmpp.Component)
if err != nil { if !ok {
log.WithFields(log.Fields{ log.Error("Not a component")
"from": from, return
}).Error(errors.Wrap(err, "Invalid from JID!")) }
return "", "", false
answer, err := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult,
From: iq.To,
To: iq.From,
Id: iq.Id,
Lang: "en",
})
if err != nil {
log.Errorf("Failed to create answer IQ: %v", err)
return
}
var login string
bare, _, ok := gateway.SplitJID(iq.From)
if ok {
session, ok := sessions[bare]
if ok {
login = session.Session.Login
}
}
var query stanza.IQPayload
if login == "" {
query = extensions.QueryRegister{
Instructions: fmt.Sprintf("Authorization in Telegram is a multi-step process, so please accept %v to your contacts and follow further instructions (provide the authentication code there, etc.).\nFor now, please provide your login.", iq.To),
}
} else {
query = extensions.QueryRegister{
Instructions: "Already logged in",
Username: login,
Registered: &extensions.QueryRegisterRegistered{},
}
}
answer.Payload = query
log.Debugf("%#v", query)
_ = gateway.ResumableSend(component, answer)
if login == "" {
gateway.SubscribeToTransport(component, iq.From)
}
}
func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) {
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
answer, err := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult,
From: iq.To,
To: iq.From,
Id: iq.Id,
Lang: "en",
})
if err != nil {
log.Errorf("Failed to create answer IQ: %v", err)
return
}
defer gateway.ResumableSend(component, answer)
if query.Remove != nil {
iqAnswerSetError(answer, query, 405)
return
}
var login string
var session *telegram.Client
bare, resource, ok := gateway.SplitJID(iq.From)
if ok {
session, ok = sessions[bare]
if ok {
login = session.Session.Login
}
}
if login == "" {
if !ok {
session, ok = getTelegramInstance(bare, &persistence.Session{}, component)
if !ok {
iqAnswerSetError(answer, query, 500)
return
}
}
err := session.TryLogin(resource, query.Username)
if err != nil {
if err.Error() == telegram.TelegramAuthDone {
iqAnswerSetError(answer, query, 406)
} else {
iqAnswerSetError(answer, query, 500)
}
return
}
err = session.SetPhoneNumber(query.Username)
if err != nil {
iqAnswerSetError(answer, query, 500)
return
}
// everything okay, the response should be empty with no payload/error at this point
gateway.SubscribeToTransport(component, iq.From)
} else {
iqAnswerSetError(answer, query, 406)
}
}
func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) {
answer.Type = stanza.IQTypeError
answer.Payload = *payload
switch code {
case 400:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeModify,
Reason: "bad-request",
}
case 405:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeCancel,
Reason: "not-allowed",
Text: "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport",
}
case 406:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeModify,
Reason: "not-acceptable",
Text: "Phone number already provided, chat with the transport for further instruction",
}
case 500:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeWait,
Reason: "internal-server-error",
}
default:
log.Error("Unknown error code, falling back with empty reason")
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeCancel,
Reason: "undefined-condition",
}
} }
return fromJid.Bare(), fromJid.Resource, true
} }
func toToID(to string) (int64, bool) { func toToID(to string) (int64, bool) {
@ -454,47 +728,69 @@ func toToID(to string) (int64, bool) {
return toID, true return toID, true
} }
func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, info string) stanza.IQPayload { 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())
}
}
if typ == TypeVCardTemp { if typ == TypeVCardTemp {
vcard := &extensions.IqVcardTemp{} vcard := &extensions.IqVcardTemp{}
vcard.Fn.Text = fn vcard.Fn.Text = info.Fn
if photo != "" { if base64Photo != "" {
vcard.Photo.Type.Text = "image/jpeg" vcard.Photo.Type.Text = "image/jpeg"
vcard.Photo.Binval.Text = photo vcard.Photo.Binval.Text = base64Photo
} }
vcard.Nickname.Text = nickname vcard.Nickname.Text = strings.Join(info.Nicknames, ",")
vcard.N.Given.Text = given vcard.N.Given.Text = info.Given
vcard.N.Family.Text = family vcard.N.Family.Text = info.Family
vcard.Tel.Number.Text = tel vcard.Tel.Number.Text = info.Tel
vcard.Desc.Text = info vcard.Desc.Text = info.Info
return vcard return vcard
} else if typ == TypeVCard4 { } else if typ == TypeVCard4 {
nodes := []stanza.Node{} nodes := []stanza.Node{}
if fn != "" { if info.Fn != "" {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "fn"}, XMLName: xml.Name{Local: "fn"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "text"}, XMLName: xml.Name{Local: "text"},
Content: fn, Content: info.Fn,
}, },
}, },
}) })
} }
if photo != "" { if base64Photo != "" {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "photo"}, XMLName: xml.Name{Local: "photo"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "uri"}, XMLName: xml.Name{Local: "uri"},
Content: "data:image/jpeg;base64," + photo, Content: "data:image/jpeg;base64," + base64Photo,
}, },
}, },
}) })
} }
if nickname != "" { for _, nickname := range info.Nicknames {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "nickname"}, XMLName: xml.Name{Local: "nickname"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
@ -513,39 +809,39 @@ func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, inf
}, },
}) })
} }
if family != "" || given != "" { if info.Family != "" || info.Given != "" {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "n"}, XMLName: xml.Name{Local: "n"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "surname"}, XMLName: xml.Name{Local: "surname"},
Content: family, Content: info.Family,
}, },
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "given"}, XMLName: xml.Name{Local: "given"},
Content: given, Content: info.Given,
}, },
}, },
}) })
} }
if tel != "" { if info.Tel != "" {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "tel"}, XMLName: xml.Name{Local: "tel"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "uri"}, XMLName: xml.Name{Local: "uri"},
Content: "tel:" + tel, Content: "tel:" + info.Tel,
}, },
}, },
}) })
} }
if info != "" { if info.Info != "" {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "note"}, XMLName: xml.Name{Local: "note"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "text"}, XMLName: xml.Name{Local: "text"},
Content: info, Content: info.Info,
}, },
}, },
}) })