Compare commits

..

25 Commits
master ... muc

Author SHA1 Message Date
Bohdan Horbeshko 4972cb6d5e Reject MUC nickname change attempts
7 months ago
Bohdan Horbeshko 1e7e761c6c Reflect name change of Telegram user in all MUCs
7 months ago
Bohdan Horbeshko b8a57c06b6 Handle MUC PM attempts
7 months ago
Bohdan Horbeshko 02578440cd Detect the "Have no write access to the chat" error from Telegram
7 months ago
Bohdan Horbeshko 47fa7bca49 Return outgoing message errors as message error stanzas (only in groupchats yet)
7 months ago
Bohdan Horbeshko a0803123b2 Advertise muc#stable_id feature
7 months ago
Bohdan Horbeshko b70bb53c6d Display outgoing MUC messages
7 months ago
Bohdan Horbeshko 41503c7fd4 Return registration-required instead of not-authorized
7 months ago
Bohdan Horbeshko cdaaa75c96 Send last pinned message as subject on MUC join
7 months ago
Bohdan Horbeshko b68c07025d Add MUC history limit (maxstanzas only)
8 months ago
Bohdan Horbeshko e8bde73164 Original sender JID in MUCs (why?)
8 months ago
Bohdan Horbeshko e77caf2c42 Send recent history on MUC join
8 months ago
Bohdan Horbeshko c1887e5a1e Fix returning MUC join errors
8 months ago
Bohdan Horbeshko 93abbe834e Send real JID for room occupants
8 months ago
Bohdan Horbeshko 6c65ef9988 Send the own MUC member the last with status codes 110/210 according to the spec
8 months ago
Bohdan Horbeshko 4249a8bf41 Suppress nickname presences for MUCs better
8 months ago
Bohdan Horbeshko f99f4f6acc Send memberlist on MUC join, suppress PM statuses for MUC JIDs
8 months ago
Bohdan Horbeshko 776993894a Merge hotfix: remove redundant "registered" identity
8 months ago
Bohdan Horbeshko 9dbd487dae Merge branch 'master' into muc
8 months ago
Bohdan Horbeshko 7eaf28ad7c Advertise gateway first, MUC next
2 years ago
Bohdan Horbeshko 63521b8f90 Extended room disco info
2 years ago
Bohdan Horbeshko 7ef32096af Basic room disco info
2 years ago
Bohdan Horbeshko 63f12202d0 Refactoring: merge handleGetDiscoInfo/handleGetDiscoItems back into one function
2 years ago
Bohdan Horbeshko 6abb7ff9c2 Respond to disco with conference identity and groups list
2 years ago
Bohdan Horbeshko afa21e10be Add a muc option (useless yet)
2 years ago

1
.gitignore vendored

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

@ -29,7 +29,7 @@ WORKDIR /src
RUN make ${MAKEOPTS}
FROM scratch AS telegabber
COPY --from=build /src/release/telegabber /usr/local/bin/
COPY --from=build /src/telegabber /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/telegabber"]
FROM scratch AS binaries

@ -1,13 +1,12 @@
.PHONY: all test
COMMIT := $(shell git rev-parse --short HEAD)
TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551"
VERSION := "v1.9.3"
TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1"
VERSION := "v2.0.0-dev"
MAKEOPTS := "-j4"
all:
mkdir -p release
go build -ldflags "-X main.commit=${COMMIT}" -o release/telegabber
go build -ldflags "-X main.commit=${COMMIT}" -o telegabber
test:
go test -v ./config ./ ./telegram ./xmpp ./xmpp/gateway ./persistence ./telegram/formatter ./badger
@ -17,9 +16,3 @@ lint:
build_indocker:
docker build --build-arg "TD_COMMIT=${TD_COMMIT}" --build-arg "VERSION=${VERSION}" --build-arg "MAKEOPTS=${MAKEOPTS}" --output=release --target binaries .
build_indocker_staging:
DOCKER_BUILDKIT=1 docker build --build-arg "TD_COMMIT=${TD_COMMIT}" --build-arg "MAKEOPTS=${MAKEOPTS}" --network host --output=release --target binaries -f staging.Dockerfile .
build_tdlib:
DOCKER_BUILDKIT=1 docker build --build-arg "TD_COMMIT=${TD_COMMIT}" --build-arg "MAKEOPTS=${MAKEOPTS}" --output=tdlib --target binaries -f tdlib.Dockerfile .

@ -33,5 +33,5 @@ require (
nhooyr.io/websocket v1.6.5 // indirect
)
replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f
replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061
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

@ -1,12 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
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-tdlib v0.0.0-20231111182840-bc2f985e6268 h1:NCbc2bYuUGQsb/3z5SCIia3N34Ktwq3FwaUAfgF/WEU=
dev.narayana.im/narayana/go-tdlib v0.0.0-20231111182840-bc2f985e6268/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU=
dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 h1:CWAQT74LwQne/3Po5KXDvudu3N0FBWm3XZZZhtl5j2w=
dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061/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/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/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/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/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=

@ -39,11 +39,10 @@ type Session struct {
KeepOnline bool `yaml:":keeponline"`
RawMessages bool `yaml:":rawmessages"`
AsciiArrows bool `yaml:":asciiarrows"`
MUC bool `yaml:":muc"`
OOBMode bool `yaml:":oobmode"`
Carbons bool `yaml:":carbons"`
HideIds bool `yaml:":hideids"`
Receipts bool `yaml:":receipts"`
NativeEdits bool `yaml:":nativeedits"`
}
var configKeys = []string{
@ -51,11 +50,10 @@ var configKeys = []string{
"keeponline",
"rawmessages",
"asciiarrows",
"muc",
"oobmode",
"carbons",
"hideids",
"receipts",
"nativeedits",
}
var sessionDB *SessionsYamlDB
@ -128,16 +126,14 @@ func (s *Session) Get(key string) (string, error) {
return fromBool(s.RawMessages), nil
case "asciiarrows":
return fromBool(s.AsciiArrows), nil
case "muc":
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
case "receipts":
return fromBool(s.Receipts), nil
case "nativeedits":
return fromBool(s.NativeEdits), nil
}
return "", errors.New("Unknown session property")
@ -181,6 +177,13 @@ func (s *Session) Set(key string, value string) (string, error) {
}
s.AsciiArrows = b
return value, nil
case "muc":
b, err := toBool(value)
if err != nil {
return "", err
}
s.MUC = b
return value, nil
case "oobmode":
b, err := toBool(value)
if err != nil {
@ -202,20 +205,6 @@ func (s *Session) Set(key string, value string) (string, error) {
}
s.HideIds = b
return value, nil
case "receipts":
b, err := toBool(value)
if err != nil {
return "", err
}
s.Receipts = b
return value, nil
case "nativeedits":
b, err := toBool(value)
if err != nil {
return "", err
}
s.NativeEdits = b
return value, nil
}
return "", errors.New("Unknown session property")

@ -47,20 +47,19 @@ func TestSessionToMap(t *testing.T) {
session := Session{
Timezone: "klsf",
RawMessages: true,
MUC: true,
OOBMode: true,
Receipts: true,
}
m := session.ToMap()
sample := map[string]string{
"timezone": "klsf",
"keeponline": "false",
"muc": "true",
"rawmessages": "true",
"asciiarrows": "false",
"oobmode": "true",
"carbons": "false",
"hideids": "false",
"receipts": "true",
"nativeedits": "false",
}
if !reflect.DeepEqual(m, sample) {
t.Errorf("Map does not match the sample: %v", m)

@ -1,46 +0,0 @@
FROM golang:1.19-bullseye AS base
RUN apt-get update
RUN apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git php
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 . --target prepare_cross_compiling ${MAKEOPTS}
WORKDIR /src/
RUN php SplitSource.php
WORKDIR /build/
RUN cmake --build . ${MAKEOPTS}
RUN make install
FROM base AS cache
ARG VERSION
COPY --from=tdlib /compiled/ /usr/local/
WORKDIR /src
RUN go env -w GOCACHE=/go-cache
RUN go env -w GOMODCACHE=/gomod-cache
RUN --mount=type=cache,target=/gomod-cache \
--mount=type=bind,source=./,target=/src \
go mod download
FROM cache AS build
ARG MAKEOPTS
WORKDIR /src
RUN --mount=type=bind,source=./,target=/src,rw \
--mount=type=cache,target=/go-cache \
--mount=type=cache,target=/gomod-cache \
--mount=type=cache,destination=/src/release \
make ${MAKEOPTS}
FROM build AS release
RUN --mount=type=cache,destination=/src/release \
cp /src/release/telegabber /
FROM scratch AS binaries
COPY --from=release /telegabber /

@ -1,23 +0,0 @@
FROM golang:1.19-bullseye AS base
RUN apt-get update
RUN apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git php
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 . --target prepare_cross_compiling ${MAKEOPTS}
WORKDIR /src/
RUN php SplitSource.php
WORKDIR /build/
RUN cmake --build . ${MAKEOPTS}
RUN make install
FROM scratch AS binaries
COPY --from=tdlib /compiled/ /

@ -12,11 +12,10 @@ import (
"dev.narayana.im/narayana/telegabber/xmpp"
log "github.com/sirupsen/logrus"
"github.com/zelenin/go-tdlib/client"
goxmpp "gosrc.io/xmpp"
)
var version string = "1.9.3"
var version string = "2.0.0-dev"
var commit string
var sm *goxmpp.StreamManager
@ -61,9 +60,6 @@ func main() {
log.Fatal(err)
}
client.SetLogVerbosityLevel(&client.SetLogVerbosityLevelRequest{
NewVerbosityLevel: stringToTdlibLogConstant(config.Telegram.Loglevel),
})
SetLogrusLevel(config.XMPP.Loglevel)
log.Infof("Starting telegabber version %v", version)
@ -93,25 +89,6 @@ func main() {
}
}
var tdlibLogConstants = map[string]int32{
":fatal": 0,
":error": 1,
":warn": 2,
":info": 3,
":debug": 4,
":verbose": 5,
":all": 1023,
}
func stringToTdlibLogConstant(c string) int32 {
level, ok := tdlibLogConstants[c]
if !ok {
level = 0
}
return level
}
func exit() {
xmpp.Close(component)
close(cleanupDone)

@ -1,19 +0,0 @@
package main
import (
"testing"
)
func TestTdlibLogInfo(t *testing.T) {
tdlibConstant := stringToTdlibLogConstant(":info")
if tdlibConstant != 3 {
t.Errorf("Wrong TDlib constant for info")
}
}
func TestTdlibLogInvalid(t *testing.T) {
tdlibConstant := stringToTdlibLogConstant("ziz")
if tdlibConstant != 0 {
t.Errorf("Unknown strings should return fatal loglevel")
}
}

@ -16,12 +16,50 @@ import (
"gosrc.io/xmpp"
)
var logConstants = map[string]int32{
":fatal": 0,
":error": 1,
":warn": 2,
":info": 3,
":debug": 4,
":verbose": 5,
":all": 1023,
}
func stringToLogConstant(c string) int32 {
level, ok := logConstants[c]
if !ok {
level = 0
}
return level
}
// DelayedStatus describes an online status expiring on timeout
type DelayedStatus struct {
TimestampOnline int64
TimestampExpired int64
}
// MUCState holds MUC metadata
type MUCState struct {
Resources map[string]bool
Members map[int64]*MUCMember
}
// MUCMember represents a MUC member
type MUCMember struct {
Nickname string
Affiliation string
}
func NewMUCState() *MUCState {
return &MUCState{
Resources: make(map[string]bool),
Members: make(map[int64]*MUCMember),
}
}
// Client stores the metadata for lazily invoked TDlib instance
type Client struct {
client *client.Client
@ -34,20 +72,19 @@ type Client struct {
jid string
Session *persistence.Session
resources map[string]bool
outbox map[string]string
content *config.TelegramContentConfig
cache *cache.Cache
online bool
outbox map[string]string
editOutbox map[string]string
DelayedStatuses map[int64]*DelayedStatus
DelayedStatusesLock sync.Mutex
lastMsgHashes map[int64]uint64
lastMsgIds map[int64]string
msgHashSeed maphash.Seed
mucCache map[int64]*MUCState
locks clientLocks
SendMessageLock sync.Mutex
}
@ -57,9 +94,8 @@ type clientLocks struct {
chatMessageLocks map[int64]*sync.Mutex
resourcesLock sync.Mutex
outboxLock sync.Mutex
editOutboxLock sync.Mutex
mucCacheLock sync.Mutex
lastMsgHashesLock sync.Mutex
lastMsgIdsLock sync.RWMutex
authorizerReadLock sync.Mutex
authorizerWriteLock sync.Mutex
@ -69,6 +105,10 @@ type clientLocks struct {
func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component, session *persistence.Session) (*Client, error) {
var options []client.Option
options = append(options, client.WithLogVerbosity(&client.SetLogVerbosityLevelRequest{
NewVerbosityLevel: stringToLogConstant(conf.Loglevel),
}))
if conf.Tdlib.Client.CatchTimeout != 0 {
options = append(options, client.WithCatchTimeout(
time.Duration(conf.Tdlib.Client.CatchTimeout)*time.Second,
@ -114,14 +154,13 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component
jid: jid,
Session: session,
resources: make(map[string]bool),
outbox: make(map[string]string),
mucCache: make(map[int64]*MUCState),
content: &conf.Content,
cache: cache.NewCache(),
outbox: make(map[string]string),
editOutbox: make(map[string]string),
options: options,
DelayedStatuses: make(map[int64]*DelayedStatus),
lastMsgHashes: make(map[int64]uint64),
lastMsgIds: make(map[int64]string),
msgHashSeed: maphash.MakeSeed(),
locks: clientLocks{
chatMessageLocks: make(map[int64]*sync.Mutex),

@ -0,0 +1,19 @@
package telegram
import (
"testing"
)
func TestLogInfo(t *testing.T) {
tdlibConstant := stringToLogConstant(":info")
if tdlibConstant != 3 {
t.Errorf("Wrong TDlib constant for info")
}
}
func TestLogInvalid(t *testing.T) {
tdlibConstant := stringToLogConstant("ziz")
if tdlibConstant != 0 {
t.Errorf("Unknown strings should return fatal loglevel")
}
}

@ -185,27 +185,12 @@ func keyValueString(key, value string) string {
}
func (c *Client) unsubscribe(chatID int64) error {
args := gateway.SimplePresence(chatID, "unsubscribed")
return c.sendPresence(args...)
}
func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) {
for i := len(messages) - 1; i >= 0; i-- {
message := messages[i]
reply, _ := c.getMessageReply(message, false, true)
gateway.SendMessage(
c.jid,
strconv.FormatInt(chatID, 10),
c.formatMessage(0, 0, false, message),
strconv.FormatInt(message.Id, 10),
c.xmpp,
reply,
"",
false,
false,
)
}
return gateway.SendPresence(
c.xmpp,
c.jid,
gateway.SPFrom(strconv.FormatInt(chatID, 10)),
gateway.SPType("unsubscribed"),
)
}
func (c *Client) usernameOrIDToID(username string) (int64, error) {
@ -279,15 +264,16 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
return notOnline
}
for _, id := range c.cache.ChatsKeys() {
c.unsubscribe(id)
}
_, err := c.client.LogOut()
if err != nil {
c.forceClose()
return errors.Wrap(err, "Logout error").Error()
}
for _, id := range c.cache.ChatsKeys() {
c.unsubscribe(id)
}
c.Session.Login = ""
// cancel auth
case "cancelauth":
@ -380,7 +366,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
}
case "config":
if len(args) > 1 {
var msg string
if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" {
return "The server did not allow to enable carbons"
}
@ -391,7 +376,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
}
gateway.DirtySessions = true
return fmt.Sprintf("%s%s set to %s", msg, args[0], value)
return fmt.Sprintf("%s set to %s", args[0], value)
} else if len(args) > 0 {
value, err := c.Session.Get(args[0])
if err != nil {
@ -420,7 +405,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
text := rawCmdArguments(cmdline, 1)
_, err = c.client.ReportChat(&client.ReportChatRequest{
ChatId: contact.Id,
Reason: &client.ReportReasonCustom{},
Reason: &client.ChatReportReasonCustom{},
Text: text,
})
if err != nil {
@ -708,18 +693,18 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
}
// blacklists current user
case "block":
_, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{
_, err := c.client.ToggleMessageSenderIsBlocked(&client.ToggleMessageSenderIsBlockedRequest{
SenderId: &client.MessageSenderUser{UserId: chatID},
BlockList: &client.BlockListMain{},
IsBlocked: true,
})
if err != nil {
return err.Error(), true
}
// unblacklists current user
case "unblock":
_, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{
_, err := c.client.ToggleMessageSenderIsBlocked(&client.ToggleMessageSenderIsBlockedRequest{
SenderId: &client.MessageSenderUser{UserId: chatID},
BlockList: nil,
IsBlocked: false,
})
if err != nil {
return err.Error(), true
@ -1003,7 +988,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
return err.Error(), true
}
c.sendMessagesReverse(chatID, messages.Messages)
c.sendMessagesReverse(chatID, messages.Messages, true, "")
// get latest entries from history
case "history":
var limit int32 = 10
@ -1014,32 +999,11 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
}
}
var newMessages *client.Messages
var messages []*client.Message
var err error
var fromId int64
for _ = range make([]struct{}, limit) { // safety limit
if len(messages) > 0 {
fromId = messages[len(messages)-1].Id
}
newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{
ChatId: chatID,
FromMessageId: fromId,
Limit: limit,
})
if err != nil {
return err.Error(), true
}
messages = append(messages, newMessages.Messages...)
if len(newMessages.Messages) == 0 || len(messages) >= int(limit) {
break
}
messages, err := c.getNLastMessages(chatID, limit)
if err != nil {
return err.Error(), true
}
c.sendMessagesReverse(chatID, messages)
c.sendMessagesReverse(chatID, messages, true, "")
// chat members
case "members":
var query string

@ -2,6 +2,7 @@ package telegram
import (
"github.com/pkg/errors"
"strconv"
"time"
"dev.narayana.im/narayana/telegabber/xmpp/gateway"
@ -68,10 +69,10 @@ func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.Auth
return nil
case client.TypeAuthorizationStateLoggingOut:
return nil
return client.ErrNotSupportedAuthorizationState
case client.TypeAuthorizationStateClosing:
return nil
return client.ErrNotSupportedAuthorizationState
case client.TypeAuthorizationStateClosed:
return client.ErrNotSupportedAuthorizationState
@ -158,7 +159,7 @@ func (c *Client) Connect(resource string) error {
}
gateway.SubscribeToTransport(c.xmpp, c.jid)
c.sendPresence(gateway.SPStatus("Logged in as: " + c.Session.Login))
gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login))
}()
return nil
@ -227,8 +228,12 @@ func (c *Client) Disconnect(resource string, quit bool) bool {
// we're offline (unsubscribe if logout)
for _, id := range c.cache.ChatsKeys() {
args := gateway.SimplePresence(id, "unavailable")
c.sendPresence(args...)
gateway.SendPresence(
c.xmpp,
c.jid,
gateway.SPFrom(strconv.FormatInt(id, 10)),
gateway.SPType("unavailable"),
)
}
c.close()

@ -8,31 +8,15 @@ import (
"github.com/zelenin/go-tdlib/client"
)
type insertionType int
const (
insertionOpening insertionType = iota
insertionClosing
insertionUnpaired
)
type MarkupModeType int
const (
MarkupModeXEP0393 MarkupModeType = iota
MarkupModeMarkdown
)
// insertion is a piece of text in given position
type insertion struct {
// Insertion is a piece of text in given position
type Insertion struct {
Offset int32
Runes []rune
Type insertionType
}
// insertionStack contains the sequence of insertions
// InsertionStack contains the sequence of insertions
// from the start or from the end
type insertionStack []*insertion
type InsertionStack []*Insertion
var boldRunesMarkdown = []rune("**")
var boldRunesXEP0393 = []rune("*")
@ -40,18 +24,13 @@ var italicRunes = []rune("_")
var strikeRunesMarkdown = []rune("~~")
var strikeRunesXEP0393 = []rune("~")
var codeRunes = []rune("`")
var preRunesStart = []rune("```\n")
var preRunesEnd = []rune("\n```")
var quoteRunes = []rune("> ")
var newlineRunes = []rune("\n")
var doubleNewlineRunes = []rune("\n\n")
var newlineCode = rune(0x0000000a)
var bmpCeil = rune(0x0000ffff)
var preRuneStart = []rune("```\n")
var preRuneEnd = []rune("\n```")
// rebalance pumps all the values until the given offset to current stack (growing
// from start) from given stack (growing from end); should be called
// before any insertions to the current stack at the given offset
func (s insertionStack) rebalance(s2 insertionStack, offset int32) (insertionStack, insertionStack) {
func (s InsertionStack) rebalance(s2 InsertionStack, offset int32) (InsertionStack, InsertionStack) {
for len(s2) > 0 && s2[len(s2)-1].Offset <= offset {
s = append(s, s2[len(s2)-1])
s2 = s2[:len(s2)-1]
@ -62,10 +41,10 @@ func (s insertionStack) rebalance(s2 insertionStack, offset int32) (insertionSta
// NewIterator is a second order function that sequentially scans and returns
// stack elements; starts returning nil when elements are ended
func (s insertionStack) NewIterator() func() *insertion {
func (s InsertionStack) NewIterator() func() *Insertion {
i := -1
return func() *insertion {
return func() *Insertion {
i++
if i < len(s) {
return s[i]
@ -141,10 +120,21 @@ func MergeAdjacentEntities(entities []*client.TextEntity) []*client.TextEntity {
}
// ClaspDirectives to the following span as required by XEP-0393
func ClaspDirectives(doubledRunes []rune, entities []*client.TextEntity) []*client.TextEntity {
func ClaspDirectives(text string, entities []*client.TextEntity) []*client.TextEntity {
alignedEntities := make([]*client.TextEntity, len(entities))
copy(alignedEntities, entities)
// transform the source text into a form with uniform runes and code points,
// by duplicating the Basic Multilingual Plane
doubledRunes := make([]rune, 0, len(text)*2)
for _, cp := range text {
if cp > 0x0000ffff {
doubledRunes = append(doubledRunes, cp, cp)
} else {
doubledRunes = append(doubledRunes, cp)
}
}
for i, entity := range alignedEntities {
var dirty bool
endOffset := entity.Offset + entity.Length
@ -177,89 +167,18 @@ func ClaspDirectives(doubledRunes []rune, entities []*client.TextEntity) []*clie
return alignedEntities
}
func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) []*insertion {
return []*insertion{
&insertion{
func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) (*Insertion, *Insertion) {
return &Insertion{
Offset: entity.Offset,
Runes: lbrace,
Type: insertionOpening,
},
&insertion{
}, &Insertion{
Offset: entity.Offset + entity.Length,
Runes: rbrace,
Type: insertionClosing,
},
}
}
func quotePrependNewlines(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion {
if len(doubledRunes) == 0 {
return []*insertion{}
}
startRunes := []rune("\n> ")
if entity.Offset == 0 || doubledRunes[entity.Offset-1] == newlineCode {
startRunes = quoteRunes
}
insertions := []*insertion{
&insertion{
Offset: entity.Offset,
Runes: startRunes,
Type: insertionUnpaired,
},
}
entityEnd := entity.Offset + entity.Length
entityEndInt := int(entityEnd)
var wasNewline bool
// last newline is omitted, there's no need to put quote mark after the quote
for i := entity.Offset; i < entityEnd-1; i++ {
isNewline := doubledRunes[i] == newlineCode
if (isNewline && markupMode == MarkupModeXEP0393) || (wasNewline && isNewline && markupMode == MarkupModeMarkdown) {
insertions = append(insertions, &insertion{
Offset: i + 1,
Runes: quoteRunes,
Type: insertionUnpaired,
})
}
if isNewline {
wasNewline = true
} else {
wasNewline = false
}
}
var rbrace []rune
if len(doubledRunes) > entityEndInt {
if doubledRunes[entityEnd] == newlineCode {
if markupMode == MarkupModeMarkdown && len(doubledRunes) > entityEndInt+1 && doubledRunes[entityEndInt+1] != newlineCode {
rbrace = newlineRunes
}
} else {
if markupMode == MarkupModeMarkdown {
rbrace = doubleNewlineRunes
} else {
rbrace = newlineRunes
}
}
}
insertions = append(insertions, &insertion{
Offset: entityEnd,
Runes: rbrace,
Type: insertionClosing,
})
return insertions
}
// entityToMarkdown generates the wrapping Markdown tags
func entityToMarkdown(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion {
if entity == nil || entity.Type == nil {
return []*insertion{}
}
// EntityToMarkdown generates the wrapping Markdown tags
func EntityToMarkdown(entity *client.TextEntity) (*Insertion, *Insertion) {
switch entity.Type.TextEntityTypeType() {
case client.TypeTextEntityTypeBold:
return markupBraces(entity, boldRunesMarkdown, boldRunesMarkdown)
@ -270,24 +189,22 @@ func entityToMarkdown(entity *client.TextEntity, doubledRunes []rune, markupMode
case client.TypeTextEntityTypeCode:
return markupBraces(entity, codeRunes, codeRunes)
case client.TypeTextEntityTypePre:
return markupBraces(entity, preRunesStart, preRunesEnd)
return markupBraces(entity, preRuneStart, preRuneEnd)
case client.TypeTextEntityTypePreCode:
preCode, _ := entity.Type.(*client.TextEntityTypePreCode)
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), preRunesEnd)
case client.TypeTextEntityTypeBlockQuote:
return quotePrependNewlines(entity, doubledRunes, MarkupModeMarkdown)
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes)
case client.TypeTextEntityTypeTextUrl:
textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl)
return markupBraces(entity, []rune("["), []rune("]("+textURL.Url+")"))
}
return []*insertion{}
return nil, nil
}
// entityToXEP0393 generates the wrapping XEP-0393 tags
func entityToXEP0393(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion {
// EntityToXEP0393 generates the wrapping XEP-0393 tags
func EntityToXEP0393(entity *client.TextEntity) (*Insertion, *Insertion) {
if entity == nil || entity.Type == nil {
return []*insertion{}
return nil, nil
}
switch entity.Type.TextEntityTypeType() {
@ -300,59 +217,33 @@ func entityToXEP0393(entity *client.TextEntity, doubledRunes []rune, markupMode
case client.TypeTextEntityTypeCode:
return markupBraces(entity, codeRunes, codeRunes)
case client.TypeTextEntityTypePre:
return markupBraces(entity, preRunesStart, preRunesEnd)
return markupBraces(entity, preRuneStart, preRuneEnd)
case client.TypeTextEntityTypePreCode:
preCode, _ := entity.Type.(*client.TextEntityTypePreCode)
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), preRunesEnd)
case client.TypeTextEntityTypeBlockQuote:
return quotePrependNewlines(entity, doubledRunes, MarkupModeXEP0393)
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes)
case client.TypeTextEntityTypeTextUrl:
textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl)
// non-standard, Pidgin-specific
return markupBraces(entity, []rune{}, []rune(" <"+textURL.Url+">"))
}
return []*insertion{}
}
// transform the source text into a form with uniform runes and code points,
// by duplicating anything beyond the Basic Multilingual Plane
func textToDoubledRunes(text string) []rune {
doubledRunes := make([]rune, 0, len(text)*2)
for _, cp := range text {
if cp > bmpCeil {
doubledRunes = append(doubledRunes, cp, cp)
} else {
doubledRunes = append(doubledRunes, cp)
}
}
return doubledRunes
return nil, nil
}
// Format traverses an already sorted list of entities and wraps the text in a markup
func Format(
sourceText string,
entities []*client.TextEntity,
markupMode MarkupModeType,
entityToMarkup func(*client.TextEntity) (*Insertion, *Insertion),
) string {
if len(entities) == 0 {
return sourceText
}
var entityToMarkup func(*client.TextEntity, []rune, MarkupModeType) []*insertion
if markupMode == MarkupModeXEP0393 {
entityToMarkup = entityToXEP0393
} else {
entityToMarkup = entityToMarkdown
}
doubledRunes := textToDoubledRunes(sourceText)
mergedEntities := SortEntities(ClaspDirectives(sourceText, MergeAdjacentEntities(SortEntities(entities))))
mergedEntities := SortEntities(ClaspDirectives(doubledRunes, MergeAdjacentEntities(SortEntities(entities))))
startStack := make(insertionStack, 0, len(sourceText))
endStack := make(insertionStack, 0, len(sourceText))
startStack := make(InsertionStack, 0, len(sourceText))
endStack := make(InsertionStack, 0, len(sourceText))
// convert entities to a stack of brackets
var maxEndOffset int32
@ -369,70 +260,36 @@ func Format(
startStack, endStack = startStack.rebalance(endStack, entity.Offset)
insertions := entityToMarkup(entity, doubledRunes, markupMode)
if len(insertions) > 1 {
startStack = append(startStack, insertions[0:len(insertions)-1]...)
startInsertion, endInsertion := entityToMarkup(entity)
if startInsertion != nil {
startStack = append(startStack, startInsertion)
}
if len(insertions) > 0 {
endStack = append(endStack, insertions[len(insertions)-1])
if endInsertion != nil {
endStack = append(endStack, endInsertion)
}
}
// flush the closing brackets that still remain in endStack
startStack, endStack = startStack.rebalance(endStack, maxEndOffset)
// sort unpaired insertions
sort.SliceStable(startStack, func(i int, j int) bool {
ins1 := startStack[i]
ins2 := startStack[j]
if ins1.Type == insertionUnpaired && ins2.Type == insertionUnpaired {
return ins1.Offset < ins2.Offset
}
if ins1.Type == insertionUnpaired {
if ins1.Offset == ins2.Offset {
if ins2.Type == insertionOpening { // > **
return true
} else if ins2.Type == insertionClosing { // **>
return false
}
} else {
return ins1.Offset < ins2.Offset
}
}
if ins2.Type == insertionUnpaired {
if ins1.Offset == ins2.Offset {
if ins1.Type == insertionOpening { // > **
return false
} else if ins1.Type == insertionClosing { // **>
return true
}
} else {
return ins1.Offset < ins2.Offset
}
}
return false
})
// merge brackets into text
markupRunes := make([]rune, 0, len(sourceText))
nextInsertion := startStack.NewIterator()
insertion := nextInsertion()
var skipNext bool
var runeI int32
for i, cp := range doubledRunes {
if skipNext {
skipNext = false
continue
}
for insertion != nil && int(insertion.Offset) <= i {
for _, cp := range sourceText {
for insertion != nil && insertion.Offset <= runeI {
markupRunes = append(markupRunes, insertion.Runes...)
insertion = nextInsertion()
}
markupRunes = append(markupRunes, cp)
// skip two UTF-16 code units (not points actually!) if needed
if cp > bmpCeil {
skipNext = true
if cp > 0x0000ffff {
runeI += 2
} else {
runeI++
}
}
for insertion != nil {

@ -7,7 +7,7 @@ import (
)
func TestNoFormatting(t *testing.T) {
markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeMarkdown)
markup := Format("abc\ndef", []*client.TextEntity{}, EntityToMarkdown)
if markup != "abc\ndef" {
t.Errorf("No formatting expected, but: %v", markup)
}
@ -20,7 +20,7 @@ func TestFormattingSimple(t *testing.T) {
Length: 4,
Type: &client.TextEntityTypeBold{},
},
}, MarkupModeMarkdown)
}, EntityToMarkdown)
if markup != "👙**🐧🐖**" {
t.Errorf("Wrong simple formatting: %v", markup)
}
@ -40,7 +40,7 @@ func TestFormattingAdjacent(t *testing.T) {
Url: "https://narayana.im/",
},
},
}, MarkupModeMarkdown)
}, EntityToMarkdown)
if markup != "a👙_🐧_[🐖](https://narayana.im/)" {
t.Errorf("Wrong adjacent formatting: %v", markup)
}
@ -63,18 +63,18 @@ func TestFormattingAdjacentAndNested(t *testing.T) {
Length: 2,
Type: &client.TextEntityTypeItalic{},
},
}, MarkupModeMarkdown)
}, EntityToMarkdown)
if markup != "```\n**👙**🐧\n```_🐖_" {
t.Errorf("Wrong adjacent&nested formatting: %v", markup)
}
}
func TestRebalanceTwoZero(t *testing.T) {
s1 := insertionStack{
&insertion{Offset: 7},
&insertion{Offset: 8},
s1 := InsertionStack{
&Insertion{Offset: 7},
&Insertion{Offset: 8},
}
s2 := insertionStack{}
s2 := InsertionStack{}
s1, s2 = s1.rebalance(s2, 7)
if !(len(s1) == 2 && len(s2) == 0 && s1[0].Offset == 7 && s1[1].Offset == 8) {
t.Errorf("Wrong rebalance 20: %#v %#v", s1, s2)
@ -82,13 +82,13 @@ func TestRebalanceTwoZero(t *testing.T) {
}
func TestRebalanceNeeded(t *testing.T) {
s1 := insertionStack{
&insertion{Offset: 7},
&insertion{Offset: 8},
s1 := InsertionStack{
&Insertion{Offset: 7},
&Insertion{Offset: 8},
}
s2 := insertionStack{
&insertion{Offset: 10},
&insertion{Offset: 9},
s2 := InsertionStack{
&Insertion{Offset: 10},
&Insertion{Offset: 9},
}
s1, s2 = s1.rebalance(s2, 9)
if !(len(s1) == 3 && len(s2) == 1 &&
@ -99,13 +99,13 @@ func TestRebalanceNeeded(t *testing.T) {
}
func TestRebalanceNotNeeded(t *testing.T) {
s1 := insertionStack{
&insertion{Offset: 7},
&insertion{Offset: 8},
s1 := InsertionStack{
&Insertion{Offset: 7},
&Insertion{Offset: 8},
}
s2 := insertionStack{
&insertion{Offset: 10},
&insertion{Offset: 9},
s2 := InsertionStack{
&Insertion{Offset: 10},
&Insertion{Offset: 9},
}
s1, s2 = s1.rebalance(s2, 8)
if !(len(s1) == 2 && len(s2) == 2 &&
@ -116,13 +116,13 @@ func TestRebalanceNotNeeded(t *testing.T) {
}
func TestRebalanceLate(t *testing.T) {
s1 := insertionStack{
&insertion{Offset: 7},
&insertion{Offset: 8},
s1 := InsertionStack{
&Insertion{Offset: 7},
&Insertion{Offset: 8},
}
s2 := insertionStack{
&insertion{Offset: 10},
&insertion{Offset: 9},
s2 := InsertionStack{
&Insertion{Offset: 10},
&Insertion{Offset: 9},
}
s1, s2 = s1.rebalance(s2, 10)
if !(len(s1) == 4 && len(s2) == 0 &&
@ -133,7 +133,7 @@ func TestRebalanceLate(t *testing.T) {
}
func TestIteratorEmpty(t *testing.T) {
s := insertionStack{}
s := InsertionStack{}
g := s.NewIterator()
v := g()
if v != nil {
@ -142,9 +142,9 @@ func TestIteratorEmpty(t *testing.T) {
}
func TestIterator(t *testing.T) {
s := insertionStack{
&insertion{Offset: 7},
&insertion{Offset: 8},
s := InsertionStack{
&Insertion{Offset: 7},
&Insertion{Offset: 8},
}
g := s.NewIterator()
v := g()
@ -208,7 +208,7 @@ func TestSortEmpty(t *testing.T) {
}
func TestNoFormattingXEP0393(t *testing.T) {
markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeXEP0393)
markup := Format("abc\ndef", []*client.TextEntity{}, EntityToXEP0393)
if markup != "abc\ndef" {
t.Errorf("No formatting expected, but: %v", markup)
}
@ -221,7 +221,7 @@ func TestFormattingXEP0393Simple(t *testing.T) {
Length: 4,
Type: &client.TextEntityTypeBold{},
},
}, MarkupModeXEP0393)
}, EntityToXEP0393)
if markup != "👙*🐧🐖*" {
t.Errorf("Wrong simple formatting: %v", markup)
}
@ -241,7 +241,7 @@ func TestFormattingXEP0393Adjacent(t *testing.T) {
Url: "https://narayana.im/",
},
},
}, MarkupModeXEP0393)
}, EntityToXEP0393)
if markup != "a👙_🐧_🐖 <https://narayana.im/>" {
t.Errorf("Wrong adjacent formatting: %v", markup)
}
@ -264,7 +264,7 @@ func TestFormattingXEP0393AdjacentAndNested(t *testing.T) {
Length: 2,
Type: &client.TextEntityTypeItalic{},
},
}, MarkupModeXEP0393)
}, EntityToXEP0393)
if markup != "```\n*👙*🐧\n```_🐖_" {
t.Errorf("Wrong adjacent&nested formatting: %v", markup)
}
@ -287,7 +287,7 @@ func TestFormattingXEP0393AdjacentItalicBoldItalic(t *testing.T) {
Length: 69,
Type: &client.TextEntityTypeItalic{},
},
}, MarkupModeXEP0393)
}, EntityToXEP0393)
if markup != "_раса двуногих крысолюдей, *которую так редко замечают, что многие отрицают само их существование*_" {
t.Errorf("Wrong adjacent italic/bold-italic formatting: %v", markup)
}
@ -315,7 +315,7 @@ func TestFormattingXEP0393MultipleAdjacent(t *testing.T) {
Length: 1,
Type: &client.TextEntityTypeItalic{},
},
}, MarkupModeXEP0393)
}, EntityToXEP0393)
if markup != "a*bcd*_e_" {
t.Errorf("Wrong multiple adjacent formatting: %v", markup)
}
@ -343,7 +343,7 @@ func TestFormattingXEP0393Intersecting(t *testing.T) {
Length: 1,
Type: &client.TextEntityTypeBold{},
},
}, MarkupModeXEP0393)
}, EntityToXEP0393)
if markup != "a*b*_*cd*e_" {
t.Errorf("Wrong intersecting formatting: %v", markup)
}
@ -361,7 +361,7 @@ func TestFormattingXEP0393InlineCode(t *testing.T) {
Length: 25,
Type: &client.TextEntityTypePre{},
},
}, MarkupModeXEP0393)
}, EntityToXEP0393)
if markup != "Is `Gajim` a thing?\n\n```\necho 'Hello'\necho 'world'\n```\n\nhruck(" {
t.Errorf("Wrong intersecting formatting: %v", markup)
}
@ -374,7 +374,7 @@ func TestFormattingMarkdownStrikethrough(t *testing.T) {
Length: 3,
Type: &client.TextEntityTypeStrikethrough{},
},
}, MarkupModeMarkdown)
}, EntityToMarkdown)
if markup != "Everyone ~~dis~~likes cake." {
t.Errorf("Wrong strikethrough formatting: %v", markup)
}
@ -387,14 +387,14 @@ func TestFormattingXEP0393Strikethrough(t *testing.T) {
Length: 3,
Type: &client.TextEntityTypeStrikethrough{},
},
}, MarkupModeXEP0393)
}, EntityToXEP0393)
if markup != "Everyone ~dis~likes cake." {
t.Errorf("Wrong strikethrough formatting: %v", markup)
}
}
func TestClaspLeft(t *testing.T) {
text := textToDoubledRunes("a b c")
text := "a b c"
entities := []*client.TextEntity{
&client.TextEntity{
Offset: 1,
@ -409,7 +409,7 @@ func TestClaspLeft(t *testing.T) {
}
func TestClaspBoth(t *testing.T) {
text := textToDoubledRunes("a b c")
text := "a b c"
entities := []*client.TextEntity{
&client.TextEntity{
Offset: 1,
@ -424,7 +424,7 @@ func TestClaspBoth(t *testing.T) {
}
func TestClaspNotNeeded(t *testing.T) {
text := textToDoubledRunes(" abc ")
text := " abc "
entities := []*client.TextEntity{
&client.TextEntity{
Offset: 1,
@ -439,7 +439,7 @@ func TestClaspNotNeeded(t *testing.T) {
}
func TestClaspNested(t *testing.T) {
text := textToDoubledRunes("a b c")
text := "a b c"
entities := []*client.TextEntity{
&client.TextEntity{
Offset: 1,
@ -459,7 +459,7 @@ func TestClaspNested(t *testing.T) {
}
func TestClaspEmoji(t *testing.T) {
text := textToDoubledRunes("a 🐖 c")
text := "a 🐖 c"
entities := []*client.TextEntity{
&client.TextEntity{
Offset: 1,
@ -472,111 +472,3 @@ func TestClaspEmoji(t *testing.T) {
t.Errorf("Wrong claspemoji: %#v", entities)
}
}
func TestNoNewlineBlockquoteXEP0393(t *testing.T) {
markup := Format("yes it can i think", []*client.TextEntity{
&client.TextEntity{
Offset: 4,
Length: 6,
Type: &client.TextEntityTypeBlockQuote{},
},
}, MarkupModeXEP0393)
if markup != "yes \n> it can\n i think" {
t.Errorf("Wrong blockquote formatting: %v", markup)
}
}
func TestNoNewlineBlockquoteMarkdown(t *testing.T) {
markup := Format("yes it can i think", []*client.TextEntity{
&client.TextEntity{
Offset: 4,
Length: 6,
Type: &client.TextEntityTypeBlockQuote{},
},
}, MarkupModeMarkdown)
if markup != "yes \n> it can\n\n i think" {
t.Errorf("Wrong blockquote formatting: %v", markup)
}
}
func TestMultilineBlockquoteXEP0393(t *testing.T) {
markup := Format("hruck\npuck\n\nshuck\ntext", []*client.TextEntity{
&client.TextEntity{
Offset: 0,
Length: 17,
Type: &client.TextEntityTypeBlockQuote{},
},
}, MarkupModeXEP0393)
if markup != "> hruck\n> puck\n> \n> shuck\ntext" {
t.Errorf("Wrong blockquote formatting: %v", markup)
}
}
func TestMultilineBlockquoteMarkdown(t *testing.T) {
markup := Format("hruck\npuck\n\nshuck\ntext", []*client.TextEntity{
&client.TextEntity{
Offset: 0,
Length: 17,
Type: &client.TextEntityTypeBlockQuote{},
},
}, MarkupModeMarkdown)
if markup != "> hruck\npuck\n\n> shuck\n\ntext" {
t.Errorf("Wrong blockquote formatting: %v", markup)
}
}
func TestMixedBlockquoteXEP0393(t *testing.T) {
markup := Format("hruck\npuck\nshuck\ntext", []*client.TextEntity{
&client.TextEntity{
Offset: 0,
Length: 16,
Type: &client.TextEntityTypeBlockQuote{},
},
&client.TextEntity{
Offset: 0,
Length: 16,
Type: &client.TextEntityTypeBold{},
},
&client.TextEntity{
Offset: 0,
Length: 10,
Type: &client.TextEntityTypeItalic{},
},
&client.TextEntity{
Offset: 7,
Length: 2,
Type: &client.TextEntityTypeStrikethrough{},
},
}, MarkupModeXEP0393)
if markup != "> *_hruck\n> p~uc~k_\n> shuck*\ntext" {
t.Errorf("Wrong blockquote formatting: %v", markup)
}
}
func TestMixedBlockquoteMarkdown(t *testing.T) {
markup := Format("hruck\npuck\nshuck\ntext", []*client.TextEntity{
&client.TextEntity{
Offset: 0,
Length: 16,
Type: &client.TextEntityTypeBlockQuote{},
},
&client.TextEntity{
Offset: 0,
Length: 16,
Type: &client.TextEntityTypeBold{},
},
&client.TextEntity{
Offset: 0,
Length: 10,
Type: &client.TextEntityTypeItalic{},
},
&client.TextEntity{
Offset: 7,
Length: 2,
Type: &client.TextEntityTypeStrikethrough{},
},
}, MarkupModeMarkdown)
if markup != "> **_hruck\np~~uc~~k_\nshuck**\n\ntext" {
t.Errorf("Wrong blockquote formatting: %v", markup)
}
}

@ -55,31 +55,6 @@ func (c *Client) cleanTempFile(path string) {
}
}
func (c *Client) sendMarker(chatId, messageId int64, typ gateway.MarkerType) {
xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatId, messageId)
if err != nil {
xmppId = strconv.FormatInt(messageId, 10)
}
var stringType string
if typ == gateway.MarkerTypeReceived {
stringType = "received"
} else if typ == gateway.MarkerTypeDisplayed {
stringType = "displayed"
}
log.WithFields(log.Fields{
"xmppId": xmppId,
}).Debugf("marker: %s", stringType)
gateway.SendMessageMarker(
c.jid,
strconv.FormatInt(chatId, 10),
c.xmpp,
typ,
xmppId,
)
}
func (c *Client) updateHandler() {
listener := c.client.GetListener()
defer listener.Close()
@ -166,12 +141,6 @@ func (c *Client) updateHandler() {
uhOh()
}
c.updateChatTitle(typedUpdate)
case client.TypeUpdateChatReadOutbox:
typedUpdate, ok := update.(*client.UpdateChatReadOutbox)
if !ok {
uhOh()
}
c.updateChatReadOutbox(typedUpdate)
default:
// log only handled types
continue
@ -184,6 +153,13 @@ func (c *Client) updateHandler() {
// new user discovered
func (c *Client) updateUser(update *client.UpdateUser) {
// check if MUC nicknames should be updated
cacheUser, ok := c.cache.GetUser(update.User.Id)
if ok && (cacheUser.FirstName != update.User.FirstName || cacheUser.LastName != update.User.LastName) {
newNickname := c.GetMUCNickname(update.User.Id)
c.updateMUCsNickname(update.User.Id, newNickname)
}
c.cache.SetUser(update.User.Id, update.User)
show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id)
go c.ProcessStatusUpdate(update.User.Id, status, show, gateway.SPType(presenceType))
@ -242,8 +218,6 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) {
lock.Lock()
defer lock.Unlock()
c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content)
// ignore self outgoing messages
if update.Message.IsOutgoing &&
update.Message.SendingState != nil &&
@ -256,6 +230,8 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) {
}).Warn("New message from chat")
c.ProcessIncomingMessage(chatId, update.Message)
c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content)
}()
}
@ -265,18 +241,12 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
defer c.updateLastMessageHash(update.ChatId, update.MessageId, update.NewContent)
log.Debugf("newContent: %#v", update.NewContent)
lock := c.getChatMessageLock(update.ChatId)
lock.Lock()
lock.Unlock()
c.SendMessageLock.Lock()
c.SendMessageLock.Unlock()
xmppId, xmppIdErr := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId)
xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId)
var ignoredResource string
if xmppIdErr == nil {
ignoredResource = c.popFromEditOutbox(xmppId)
if err == nil {
ignoredResource = c.popFromOutbox(xmppId)
} else {
log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.MessageId)
}
@ -290,62 +260,19 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
if update.NewContent.MessageContentType() == client.TypeMessageText && c.hasLastMessageHashChanged(update.ChatId, update.MessageId, update.NewContent) {
textContent := update.NewContent.(*client.MessageText)
log.Debugf("textContent: %#v", textContent.Text)
var replaceId string
sId := strconv.FormatInt(update.MessageId, 10)
var isCarbon bool
// use XEP-0308 edits only if the last message is edited for sure, fallback otherwise
if c.Session.NativeEdits {
lastXmppId, ok := c.getLastChatMessageId(update.ChatId)
if xmppIdErr != nil {
xmppId = sId
}
if ok && lastXmppId == xmppId {
replaceId = xmppId
} else {
log.Infof("Mismatching message ids: %v %v, falling back to separate edit message", lastXmppId, xmppId)
}
}
message, messageErr := c.client.GetMessage(&client.GetMessageRequest{
ChatId: update.ChatId,
MessageId: update.MessageId,
})
var prefix string
if messageErr == nil {
isCarbon = c.isCarbonsEnabled() && message.IsOutgoing
// reply correction support in clients is suboptimal yet, so cut them out for now
prefix, _ = c.messageToPrefix(message, "", "", true)
var editChar string
if c.Session.AsciiArrows {
editChar = "e "
} else {
log.Errorf("No message %v/%v found, cannot reliably determine if it's a carbon", update.ChatId, update.MessageId)
}
var text strings.Builder
if replaceId == "" {
var editChar string
if c.Session.AsciiArrows {
editChar = "e"
} else {
editChar = "✎"
}
text.WriteString(fmt.Sprintf("%s %v | ", editChar, update.MessageId))
} else if prefix != "" {
text.WriteString(prefix)
text.WriteString(c.getPrefixSeparator(update.ChatId))
editChar = "✎ "
}
text.WriteString(formatter.Format(
text := editChar + fmt.Sprintf("%v | %s", update.MessageId, formatter.Format(
textContent.Text.Text,
textContent.Text.Entities,
markupFunction,
))
sChatId := strconv.FormatInt(update.ChatId, 10)
for _, jid := range jids {
gateway.SendMessage(jid, sChatId, text.String(), "e"+sId, c.xmpp, nil, replaceId, isCarbon, false)
gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, 0, false, false, "")
}
}
}
@ -374,25 +301,19 @@ func (c *Client) updateAuthorizationState(update *client.UpdateAuthorizationStat
}
}
// clean uploaded files
func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) {
// replace message ID in local database
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())
}
c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content)
c.sendMarker(update.Message.ChatId, update.Message.Id, gateway.MarkerTypeReceived)
// clean uploaded files
file, _ := c.contentToFile(update.Message.Content)
if file != nil && file.Local != nil {
c.cleanTempFile(file.Local.Path)
}
}
func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed) {
// clean uploaded files
file, _ := c.contentToFile(update.Message.Content)
if file != nil && file.Local != nil {
c.cleanTempFile(file.Local.Path)
@ -401,10 +322,14 @@ func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed)
// chat title changed
func (c *Client) updateChatTitle(update *client.UpdateChatTitle) {
chat, user, _ := c.GetContactByID(update.ChatId, nil)
if c.Session.MUC && c.IsGroup(chat) {
return
}
gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp)
// set also the status (for group chats only)
chat, user, _ := c.GetContactByID(update.ChatId, nil)
if user == nil {
c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true))
}
@ -414,7 +339,3 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) {
chat.Title = update.Title
}
}
func (c *Client) updateChatReadOutbox(update *client.UpdateChatReadOutbox) {
c.sendMarker(update.ChatId, update.LastReadOutboxMessageId, gateway.MarkerTypeDisplayed)
}

File diff suppressed because it is too large Load Diff

@ -431,17 +431,20 @@ func TestMessageToPrefix1(t *testing.T) {
Id: 42,
IsOutgoing: true,
ForwardInfo: &client.MessageForwardInfo{
Origin: &client.MessageOriginHiddenUser{
Origin: &client.MessageForwardOriginHiddenUser{
SenderName: "ziz",
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", false)
prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", nil)
if prefix != "➡ 42 | fwd: ziz" {
t.Errorf("Wrong prefix: %v", prefix)
}
if gatewayReply != nil {
t.Errorf("Reply is not nil: %v", gatewayReply)
if replyStart != 0 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 0 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
}
@ -449,17 +452,20 @@ func TestMessageToPrefix2(t *testing.T) {
message := client.Message{
Id: 56,
ForwardInfo: &client.MessageForwardInfo{
Origin: &client.MessageOriginChannel{
Origin: &client.MessageForwardOriginChannel{
AuthorSignature: "zaz",
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", false)
prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", nil)
if prefix != "⬅ 56 | fwd: (zaz) | preview: y.jpg" {
t.Errorf("Wrong prefix: %v", prefix)
}
if gatewayReply != nil {
t.Errorf("Reply is not nil: %v", gatewayReply)
if replyStart != 0 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 0 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
}
@ -467,17 +473,20 @@ func TestMessageToPrefix3(t *testing.T) {
message := client.Message{
Id: 56,
ForwardInfo: &client.MessageForwardInfo{
Origin: &client.MessageOriginChannel{
Origin: &client.MessageForwardOriginChannel{
AuthorSignature: "zuz",
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg", false)
prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg", nil)
if prefix != "< 56 | fwd: (zuz) | file: a.jpg" {
t.Errorf("Wrong prefix: %v", prefix)
}
if gatewayReply != nil {
t.Errorf("Reply is not nil: %v", gatewayReply)
if replyStart != 0 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 0 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
}
@ -486,12 +495,15 @@ func TestMessageToPrefix4(t *testing.T) {
Id: 23,
IsOutgoing: true,
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false)
prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", nil)
if prefix != "> 23" {
t.Errorf("Wrong prefix: %v", prefix)
}
if gatewayReply != nil {
t.Errorf("Reply is not nil: %v", gatewayReply)
if replyStart != 0 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if replyEnd != 0 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
}
@ -499,95 +511,46 @@ func TestMessageToPrefix5(t *testing.T) {
message := client.Message{
Id: 560,
ForwardInfo: &client.MessageForwardInfo{
Origin: &client.MessageOriginChat{
Origin: &client.MessageForwardOriginChat{
AuthorSignature: "zyz",
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg", false)
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" {
t.Errorf("Wrong prefix: %v", prefix)
}
if gatewayReply != nil {
t.Errorf("Reply is not nil: %v", gatewayReply)
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,
ChatId: 25,
IsOutgoing: true,
ReplyTo: &client.MessageReplyToMessage{
ChatId: 41,
Quote: &client.TextQuote{
Text: &client.FormattedText{
Text: "tist\nuz\niz",
},
},
Origin: &client.MessageOriginHiddenUser{
SenderName: "ziz",
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false)
if prefix != "> 23 | reply: ziz @ unknown contact: TDlib instance is offline | tist uz iz" {
t.Errorf("Wrong prefix: %v", prefix)
Id: 23,
IsOutgoing: true,
ReplyToMessageId: 42,
}
if gatewayReply != nil {
t.Errorf("Reply is not nil: %v", gatewayReply)
}
}
func TestMessageToPrefix7(t *testing.T) {
message := client.Message{
Id: 23,
ChatId: 42,
IsOutgoing: true,
ReplyTo: &client.MessageReplyToMessage{
ChatId: 41,
Content: &client.MessageText{
Text: &client.FormattedText{
Text: "tist",
},
},
Origin: &client.MessageOriginChannel{
AuthorSignature: "zaz",
reply := client.Message{
Id: 42,
Content: &client.MessageText{
Text: &client.FormattedText{
Text: "tist",
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false)
if prefix != "> 23 | reply: (zaz) @ unknown contact: TDlib instance is offline | 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 gatewayReply != nil {
t.Errorf("Reply is not nil: %v", gatewayReply)
}
}
func TestMessageToPrefix8(t *testing.T) {
message := client.Message{
Id: 23,
ChatId: 42,
IsOutgoing: true,
ReplyTo: &client.MessageReplyToMessage{
ChatId: 41,
Content: &client.MessageText{
Text: &client.FormattedText{
Text: "tist",
},
},
Origin: &client.MessageOriginChannel{
AuthorSignature: "zuz",
},
},
}
prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", true)
if prefix != "> 23" {
t.Errorf("Wrong prefix: %v", prefix)
if replyStart != 4 {
t.Errorf("Wrong replyStart: %v", replyStart)
}
if gatewayReply != nil {
t.Errorf("Reply is not nil: %v", gatewayReply)
if replyEnd != 26 {
t.Errorf("Wrong replyEnd: %v", replyEnd)
}
}

@ -213,6 +213,60 @@ type QueryRegisterRemove struct {
XMLName xml.Name `xml:"remove"`
}
// PresenceXMucUserExtension is from XEP-0045
type PresenceXMucUserExtension struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"`
Item PresenceXMucUserItem
Statuses []PresenceXMucUserStatus
}
// PresenceXMucUserItem is from XEP-0045
type PresenceXMucUserItem struct {
XMLName xml.Name `xml:"item"`
Affiliation string `xml:"affiliation,attr"`
Jid string `xml:"jid,attr"`
Nick string `xml:"nick,attr,omitempty"`
Role string `xml:"role,attr"`
}
// PresenceXMucUserStatus is from XEP-0045
type PresenceXMucUserStatus struct {
XMLName xml.Name `xml:"status"`
Code uint16 `xml:"code,attr"`
}
// MessageDelay is from XEP-0203
type MessageDelay struct {
XMLName xml.Name `xml:"urn:xmpp:delay delay"`
From string `xml:"from,attr"`
Stamp string `xml:"stamp,attr"`
}
// MessageDelayLegacy is from XEP-0203
type MessageDelayLegacy struct {
XMLName xml.Name `xml:"jabber:x:delay x"`
From string `xml:"from,attr"`
Stamp string `xml:"stamp,attr"`
}
// MessageAddresses is from XEP-0033
type MessageAddresses struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/address addresses"`
Addresses []MessageAddress
}
// MessageAddress is from XEP-0033
type MessageAddress struct {
XMLName xml.Name `xml:"address"`
Type string `xml:"type,attr"`
Jid string `xml:"jid,attr"`
}
// EmptySubject is a dummy for MUCs to circumvent omitempty. Not registered as it would conflict with Subject field
type EmptySubject struct {
XMLName xml.Name `xml:"subject"`
}
// Namespace is a namespace!
func (c PresenceNickExtension) Namespace() string {
return c.XMLName.Space
@ -278,6 +332,21 @@ func (c QueryRegister) GetSet() *stanza.ResultSet {
return c.ResultSet
}
// Namespace is a namespace!
func (c PresenceXMucUserExtension) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c MessageDelay) Namespace() string {
return c.XMLName.Space
}
// Namespace is a namespace!
func (c MessageDelayLegacy) Namespace() string {
return c.XMLName.Space
}
// Name is a packet name
func (ClientMessage) Name() string {
return "message"
@ -362,4 +431,28 @@ func init() {
"jabber:iq:register",
"query",
}, QueryRegister{})
// presence muc user
stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{
"http://jabber.org/protocol/muc#user",
"x",
}, PresenceXMucUserExtension{})
// message delay
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"urn:xmpp:delay",
"delay",
}, MessageDelay{})
// legacy message delay
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"jabber:x:delay",
"x",
}, MessageDelayLegacy{})
// message addresses
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{
"http://jabber.org/protocol/address",
"addresses",
}, MessageAddresses{})
}

@ -3,9 +3,9 @@ package gateway
import (
"encoding/xml"
"github.com/pkg/errors"
"strconv"
"strings"
"sync"
"time"
"dev.narayana.im/narayana/telegabber/badger"
"dev.narayana.im/narayana/telegabber/xmpp/extensions"
@ -23,18 +23,6 @@ type Reply struct {
End uint64
}
type MarkerType byte
const (
MarkerTypeReceived MarkerType = iota
MarkerTypeDisplayed
)
type marker struct {
Type MarkerType
Id string
}
const NSNick string = "http://jabber.org/protocol/nick"
// Queue stores presences to send later
@ -55,34 +43,41 @@ var DirtySessions = false
var MessageOutgoingPermissionVersion = 0
// SendMessage creates and sends a message stanza
func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, replaceId string, isCarbon, requestReceipt bool) {
sendMessageWrapper(to, from, body, id, component, reply, nil, "", replaceId, isCarbon, requestReceipt)
func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, isCarbon, isGroupchat bool, originalFrom string) {
sendMessageWrapper(to, from, body, "", "", id, component, reply, timestamp, "", isCarbon, isGroupchat, false, originalFrom, 0)
}
// SendServiceMessage creates and sends a simple message stanza from transport
func SendServiceMessage(to string, body string, component *xmpp.Component) {
sendMessageWrapper(to, "", body, "", component, nil, nil, "", "", false, false)
func SendServiceMessage(to, body string, component *xmpp.Component) {
sendMessageWrapper(to, "", body, "", "", "", component, nil, 0, "", false, false, false, "", 0)
}
// 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, nil, "", "", false, false)
func SendTextMessage(to, from, body string, component *xmpp.Component) {
sendMessageWrapper(to, from, body, "", "", "", component, nil, 0, "", false, false, false, "", 0)
}
// SendErrorMessage creates and sends an error message stanza
func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) {
sendMessageWrapper(to, from, "", "", text, "", component, nil, 0, "", false, isGroupchat, false, "", code)
}
// SendErrorMessageWithBody creates and sends an error message stanza with body payload
func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) {
sendMessageWrapper(to, from, body, "", errorText, id, component, nil, 0, "", false, isGroupchat, false, "", code)
}
// 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, replaceId string, isCarbon, requestReceipt bool) {
sendMessageWrapper(to, from, body, id, component, reply, nil, oob, replaceId, isCarbon, requestReceipt)
func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat bool, originalFrom string) {
sendMessageWrapper(to, from, body, "", "", id, component, reply, timestamp, oob, isCarbon, isGroupchat, false, originalFrom, 0)
}
// SendMessageMarker creates and sends a message stanza with a XEP-0333 marker
func SendMessageMarker(to string, from string, component *xmpp.Component, markerType MarkerType, markerId string) {
sendMessageWrapper(to, from, "", "", component, nil, &marker{
Type: markerType,
Id: markerId,
}, "", "", false, false)
// SendSubjectMessage creates and sends a MUC subject
func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) {
sendMessageWrapper(to, from, "", subject, "", id, component, nil, timestamp, "", false, true, true, "", 0)
}
func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, marker *marker, oob, replaceId string, isCarbon, requestReceipt bool) {
func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat, forceSubject bool, originalFrom string, errorCode int) {
toJid, err := stanza.NewJid(to)
if err != nil {
log.WithFields(log.Fields{
@ -97,12 +92,17 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
var logFrom string
var messageFrom string
var messageTo string
if from == "" {
logFrom = componentJid
messageFrom = componentJid
} else {
if isGroupchat {
logFrom = from
messageFrom = from + "@" + componentJid
messageFrom = from
} else {
if from == "" {
logFrom = componentJid
messageFrom = componentJid
} else {
logFrom = from
messageFrom = from + "@" + componentJid
}
}
if isCarbon {
messageTo = messageFrom
@ -116,14 +116,51 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
"to": to,
}).Warn("Got message")
var messageType stanza.StanzaType
if errorCode != 0 {
messageType = stanza.MessageTypeError
} else if isGroupchat {
messageType = stanza.MessageTypeGroupchat
} else {
messageType = stanza.MessageTypeChat
}
message := stanza.Message{
Attrs: stanza.Attrs{
From: messageFrom,
To: messageTo,
Type: "chat",
Type: messageType,
Id: id,
},
Body: body,
Subject: subject,
Body: body,
}
if errorCode != 0 {
message.Error = stanza.Err{
Code: errorCode,
Text: errorText,
}
switch errorCode {
case 400:
message.Error.Type = stanza.ErrorTypeModify
message.Error.Reason = "bad-request"
case 403:
message.Error.Type = stanza.ErrorTypeAuth
message.Error.Reason = "forbidden"
case 404:
message.Error.Type = stanza.ErrorTypeCancel
message.Error.Reason = "item-not-found"
case 406:
message.Error.Type = stanza.ErrorTypeModify
message.Error.Reason = "not-acceptable"
case 500:
message.Error.Type = stanza.ErrorTypeWait
message.Error.Reason = "internal-server-error"
default:
log.Error("Unknown error code, falling back with empty reason")
message.Error.Type = stanza.ErrorTypeCancel
message.Error.Reason = "undefined-condition"
}
}
if oob != "" {
@ -140,22 +177,35 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End))
}
}
if marker != nil {
if marker.Type == MarkerTypeReceived {
message.Extensions = append(message.Extensions, stanza.MarkReceived{ID: marker.Id})
} else if marker.Type == MarkerTypeDisplayed {
message.Extensions = append(message.Extensions, stanza.MarkDisplayed{ID: marker.Id})
message.Extensions = append(message.Extensions, stanza.ReceiptReceived{ID: marker.Id})
}
}
if !isCarbon && toJid.Resource != "" {
if !isGroupchat && !isCarbon && toJid.Resource != "" {
message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
}
if requestReceipt {
message.Extensions = append(message.Extensions, stanza.Markable{})
if timestamp != 0 {
var delayFrom string
if isGroupchat {
delayFrom, _, _ = SplitJID(from)
}
message.Extensions = append(message.Extensions, extensions.MessageDelay{
From: delayFrom,
Stamp: time.Unix(timestamp, 0).UTC().Format(time.RFC3339),
})
message.Extensions = append(message.Extensions, extensions.MessageDelayLegacy{
From: delayFrom,
Stamp: time.Unix(timestamp, 0).UTC().Format("20060102T15:04:05"),
})
}
if originalFrom != "" {
message.Extensions = append(message.Extensions, extensions.MessageAddresses{
Addresses: []extensions.MessageAddress{
extensions.MessageAddress{
Type: "ofrom",
Jid: originalFrom,
},
},
})
}
if replaceId != "" {
message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId})
if subject == "" && forceSubject {
message.Extensions = append(message.Extensions, extensions.EmptySubject{})
}
if isCarbon {
@ -163,7 +213,7 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
Attrs: stanza.Attrs{
From: bareTo,
To: to,
Type: "chat",
Type: messageType,
},
}
carbonMessage.Extensions = append(carbonMessage.Extensions, extensions.CarbonSent{
@ -275,6 +325,18 @@ var SPResource = args.NewString()
// SPImmed skips queueing
var SPImmed = args.NewBool(args.Default(true))
// SPMUCAffiliation is a XEP-0045 MUC affiliation
var SPMUCAffiliation = args.NewString()
// SPMUCNick is a XEP-0045 MUC user nick
var SPMUCNick = args.NewString()
// SPMUCJid is a real jid of a MUC member
var SPMUCJid = args.NewString()
// SPMUCStatusCodes is a set of XEP-0045 MUC status codes
var SPMUCStatusCodes = args.New()
func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
var presenceFrom string
if SPFrom.IsSet(args) {
@ -330,6 +392,32 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
})
}
}
if SPMUCAffiliation.IsSet(args) {
affiliation := SPMUCAffiliation.Get(args)
if affiliation != "" {
userExt := extensions.PresenceXMucUserExtension{
Item: extensions.PresenceXMucUserItem{
Affiliation: affiliation,
Role: affilationToRole(affiliation),
},
}
if SPMUCNick.IsSet(args) {
userExt.Item.Nick = SPMUCNick.Get(args)
}
if SPMUCJid.IsSet(args) {
userExt.Item.Jid = SPMUCJid.Get(args)
}
if SPMUCStatusCodes.IsSet(args) {
statusCodes := SPMUCStatusCodes.Get(args).([]uint16)
for _, statusCode := range statusCodes {
userExt.Statuses = append(userExt.Statuses, extensions.PresenceXMucUserStatus{
Code: statusCode,
})
}
}
presence.Extensions = append(presence.Extensions, userExt)
}
}
return presence
}
@ -378,20 +466,6 @@ func SendPresence(component *xmpp.Component, to string, args ...args.V) error {
return nil
}
// SPAppendFrom appends numeric from and resource to varargs
func SPAppendFrom(oldArgs []args.V, id int64) []args.V {
newArgs := append(oldArgs, SPFrom(strconv.FormatInt(id, 10)))
newArgs = append(newArgs, SPResource(Jid.Resource))
return newArgs
}
// SimplePresence crafts simple presence varargs
func SimplePresence(from int64, typ string) []args.V {
args := []args.V{SPType(typ)}
args = SPAppendFrom(args, from)
return args
}
// ResumableSend tries to resume the connection once and sends the packet again
func ResumableSend(component *xmpp.Component, packet stanza.Packet) error {
err := component.Send(packet)
@ -426,3 +500,13 @@ func SplitJID(from string) (string, string, bool) {
}
return fromJid.Bare(), fromJid.Resource, true
}
func affilationToRole(affilation string) string {
switch affilation {
case "owner", "admin":
return "moderator"
case "member":
return "participant"
}
return "none"
}

@ -27,6 +27,12 @@ const (
)
const NodeVCard4 string = "urn:xmpp:vcard4"
type discoType int
const (
discoTypeInfo discoType = iota
discoTypeItems
)
func logPacketType(p stanza.Packet) {
log.Warnf("Ignoring packet: %T\n", p)
}
@ -55,12 +61,12 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
}
_, ok = iq.Payload.(*stanza.DiscoInfo)
if ok {
go handleGetDiscoInfo(s, iq)
go handleGetDisco(discoTypeInfo, s, iq)
return
}
_, ok = iq.Payload.(*stanza.DiscoItems)
if ok {
go handleGetDiscoItems(s, iq)
go handleGetDisco(discoTypeItems, s, iq)
return
}
_, ok = iq.Payload.(*extensions.QueryRegister)
@ -117,6 +123,26 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
toID, ok := toToID(msg.To)
if ok {
toJid, err := stanza.NewJid(msg.To)
if err != nil {
log.Error("Invalid to JID!")
return
}
isGroupchat := msg.Type == "groupchat"
if session.Session.MUC && toJid.Resource != "" {
chat, _, err := session.GetContactByID(toID, nil)
if err == nil && session.IsGroup(chat) {
if isGroupchat {
gateway.SendErrorMessageWithBody(msg.From, msg.To, msg.Body, "", msg.Id, 400, true, component)
} else {
gateway.SendErrorMessage(msg.From, msg.To, "PMing room members is not supported, use the real JID", 406, true, component)
}
return
}
}
var reply extensions.Reply
var fallback extensions.Fallback
var replace extensions.Replace
@ -128,7 +154,6 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
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)
@ -191,24 +216,33 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
session.SendMessageLock.Lock()
defer session.SendMessageLock.Unlock()
tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId)
if tgMessageId != 0 {
tgMessage := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId, isGroupchat)
if tgMessage != nil {
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.AddToEditOutbox(replace.Id, resource)
session.AddToOutbox(replace.Id, resource)
} else {
err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id)
if err == nil {
// session.AddToOutbox(msg.Id, resource)
session.UpdateLastChatMessageId(toID, msg.Id)
} else {
log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id)
err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessage.Id, msg.Id)
if err != nil {
log.Errorf("Failed to save ids %v/%v %v", toID, tgMessage.Id, msg.Id)
}
}
// pong groupchat messages back
if isGroupchat && toJid.Resource == "" {
session.SendMessageToGateway(
toID,
tgMessage,
msg.Id,
false,
msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage)),
[]string{msg.From},
)
}
} else {
/*
// if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway
@ -255,30 +289,6 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
gateway.MessageOutgoingPermissionVersion = 2
}
}
var displayed stanza.MarkDisplayed
msg.Get(&displayed)
if displayed.ID != "" {
log.Debugf("displayed: %#v", displayed)
bare, _, ok := gateway.SplitJID(msg.From)
if !ok {
return
}
session, ok := sessions[bare]
if !ok {
return
}
toID, ok := toToID(msg.To)
if !ok {
return
}
msgId, err := strconv.ParseInt(displayed.ID, 10, 64)
if err == nil {
session.MarkAsRead(toID, msgId)
}
return
}
}
if msg.Type == "error" {
@ -308,7 +318,15 @@ func HandlePresence(s xmpp.Sender, p stanza.Packet) {
}
if prs.To == gateway.Jid.Bare() {
handlePresence(s, prs)
return
}
var mucExt stanza.MucPresence
prs.Get(&mucExt)
if mucExt.XMLName.Space != "" {
handleMUCPresence(s, prs, mucExt)
return
}
tryHandleMUCNicknameChange(s, prs)
}
func handleSubscription(s xmpp.Sender, p stanza.Presence) {
@ -418,6 +436,141 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) {
}
}
func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresence) {
log.WithFields(log.Fields{
"type": p.Type,
"from": p.From,
"to": p.To,
}).Warn("MUC presence")
log.Debugf("%#v", p)
if p.Type == "" {
toBare, nickname, ok := gateway.SplitJID(p.To)
if ok {
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
// separate declaration is crucial for passing as pointer to defer
var reply *stanza.Presence
reply = &stanza.Presence{Attrs: stanza.Attrs{
From: toBare,
To: p.From,
Id: p.Id,
}}
defer gateway.ResumableSend(component, reply)
if nickname == "" {
presenceReplySetError(reply, 400)
return
}
chatId, ok := toToID(toBare)
if !ok {
presenceReplySetError(reply, 404)
return
}
fromBare, fromResource, ok := gateway.SplitJID(p.From)
if !ok {
presenceReplySetError(reply, 400)
return
}
session, ok := sessions[fromBare]
if !ok || !session.Session.MUC {
presenceReplySetError(reply, 407)
return
}
chat, _, err := session.GetContactByID(chatId, nil)
if err != nil || !session.IsGroup(chat) {
presenceReplySetError(reply, 404)
return
}
limit, ok := mucExt.History.MaxStanzas.Get()
if !ok {
limit = 20
}
session.JoinMUC(chatId, fromResource, int32(limit))
}
}
}
func tryHandleMUCNicknameChange(s xmpp.Sender, p stanza.Presence) {
log.WithFields(log.Fields{
"type": p.Type,
"from": p.From,
"to": p.To,
}).Warn("Nickname change presence?")
log.Debugf("%#v", p)
if p.Type != "" {
return
}
toBare, nickname, ok := gateway.SplitJID(p.To)
if !ok || nickname == "" {
return
}
fromBare, fromResource, ok := gateway.SplitJID(p.From)
if !ok {
return
}
session, ok := sessions[fromBare]
if !ok || !session.Session.MUC {
return
}
chatId, ok := toToID(toBare)
if !ok {
return
}
chat, _, err := session.GetContactByID(chatId, nil)
if err != nil || !session.IsGroup(chat) {
return
}
if !session.MUCHasResource(chatId, fromResource) {
return
}
log.Warn("🗿 Yes")
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
from := toBare
nickname, ok = session.GetMyMUCNickname(chatId)
if ok {
from = from+"/"+nickname
}
reply := &stanza.Presence{
Attrs: stanza.Attrs{
From: from,
To: p.From,
Id: p.Id,
Type: stanza.PresenceTypeError,
},
Error: stanza.Err{
Code: 406,
Type: stanza.ErrorTypeModify,
Reason: "not-acceptable",
Text: "Telegram does not support changing nicknames per-chat. Issue a /setname command to the transport if you wish to change the global name",
},
}
gateway.ResumableSend(component, reply)
}
func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) {
log.WithFields(log.Fields{
"from": iq.From,
@ -468,7 +621,7 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) {
_ = gateway.ResumableSend(component, &answer)
}
func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) {
answer, err := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult,
From: iq.To,
@ -481,43 +634,92 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
return
}
disco := answer.DiscoInfo()
_, ok := toToID(iq.To)
if ok {
disco.AddIdentity("", "account", "registered")
disco.AddFeatures(stanza.NSMsgChatMarkers)
disco.AddFeatures(stanza.NSMsgReceipts)
} else {
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
disco.AddFeatures("jabber:iq:register")
}
answer.Payload = disco
if dt == discoTypeInfo {
disco := answer.DiscoInfo()
toID, toOk := toToID(iq.To)
if !toOk {
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
disco.AddFeatures("jabber:iq:register")
}
log.Debugf("%#v", answer)
var isMuc bool
bare, _, fromOk := gateway.SplitJID(iq.From)
if fromOk {
session, sessionOk := sessions[bare]
if sessionOk && session.Session.MUC {
if toOk {
chat, _, err := session.GetContactByID(toID, nil)
if err == nil && session.IsGroup(chat) {
isMuc = true
disco.AddIdentity(chat.Title, "conference", "text")
disco.AddFeatures(
"http://jabber.org/protocol/muc",
"muc_persistent",
"muc_hidden",
"muc_membersonly",
"muc_unmoderated",
"muc_nonanonymous",
"muc_unsecured",
"http://jabber.org/protocol/muc#stable_id",
)
fields := []*stanza.Field{
&stanza.Field{
Var: "FORM_TYPE",
Type: "hidden",
ValuesList: []string{"http://jabber.org/protocol/muc#roominfo"},
},
&stanza.Field{
Var: "muc#roominfo_description",
Label: "Description",
ValuesList: []string{session.GetChatDescription(chat)},
},
&stanza.Field{
Var: "muc#roominfo_occupants",
Label: "Number of occupants",
ValuesList: []string{strconv.FormatInt(int64(session.GetChatMemberCount(chat)), 10)},
},
}
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
disco.Form = stanza.NewForm(fields, "result")
}
} else {
disco.AddFeatures(
stanza.NSDiscoItems,
"http://jabber.org/protocol/muc#stable_id",
)
disco.AddIdentity("Telegram group chats", "conference", "text")
}
}
}
if toOk && !isMuc {
disco.AddIdentity("", "account", "registered")
}
answer.Payload = disco
} else if dt == discoTypeItems {
disco := answer.DiscoItems()
_ = gateway.ResumableSend(component, answer)
}
_, ok := toToID(iq.To)
if !ok {
bare, _, ok := gateway.SplitJID(iq.From)
if ok {
// raw access, no need to create a new instance if not connected
session, ok := sessions[bare]
if ok && session.Session.MUC {
bareJid := gateway.Jid.Bare()
disco.AddItem(bareJid, "", "Telegram group chats")
for _, chat := range session.GetGroupChats() {
jid := strconv.FormatInt(chat.Id, 10) + "@" + bareJid
disco.AddItem(jid, "", chat.Title)
}
}
}
}
func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) {
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
answer.Payload = disco
}
answer.Payload = answer.DiscoItems()
log.Debugf("%#v", answer)
component, ok := s.(*xmpp.Component)
if !ok {
@ -687,6 +889,28 @@ func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code
}
}
func presenceReplySetError(reply *stanza.Presence, code int) {
reply.Type = stanza.PresenceTypeError
reply.Error = stanza.Err{
Code: code,
}
switch code {
case 400:
reply.Error.Type = stanza.ErrorTypeModify
reply.Error.Reason = "jid-malformed"
case 407:
reply.Error.Type = stanza.ErrorTypeAuth
reply.Error.Reason = "registration-required"
case 404:
reply.Error.Type = stanza.ErrorTypeCancel
reply.Error.Reason = "item-not-found"
default:
log.Error("Unknown error code, falling back with empty reason")
reply.Error.Type = stanza.ErrorTypeCancel
reply.Error.Reason = "undefined-condition"
}
}
func toToID(to string) (int64, bool) {
toParts := strings.Split(to, "@")
if len(toParts) < 2 {

Loading…
Cancel
Save