Compare commits

...

32 commits
dev ... master

Author SHA1 Message Date
Bohdan Horbeshko ba8f4c08cf Attach prefix to OOB descriptions and omit empty ones only if sender is displayed by carbon 2024-06-01 16:45:21 -04:00
Bohdan Horbeshko af07773b07 Random IDs for service messages 2024-05-10 19:22:53 -04:00
Bohdan Horbeshko a74e2bcb7d Mute/unmute whole chats with no arguments 2024-05-05 13:16:38 -04:00
Bohdan Horbeshko a3f6d5f774 Support nativeedits for rawmessages=false 2024-04-27 00:31:21 -04:00
Bohdan Horbeshko 2459b14948 Version 1.9.2 2024-04-11 22:24:22 -04:00
Bohdan Horbeshko f15e44436b Use carbons for non-native edits too 2024-04-11 20:59:49 -04:00
Bohdan Horbeshko a36856b768 Fix filtering content updates for outgoing messages 2024-04-11 20:37:51 -04:00
Bohdan Horbeshko b499992148 Fix missing read markers in other XMPP clients than the message sender 2024-04-10 22:17:58 -04:00
Bohdan Horbeshko 144c5724ea Fix module cache in staging.Dockerfile 2024-04-09 19:09:47 -04:00
Bohdan Horbeshko 3e772be7a6 Add tdlib.Dockerfile 2024-04-09 19:08:37 -04:00
Bohdan Horbeshko 908bd76aac Add staging.Dockerfile 2024-03-29 07:39:10 -04:00
Bohdan Horbeshko 67c38823f2 Avoid broken state on a failed logout attempt 2024-03-29 07:35:06 -04:00
Bohdan Horbeshko f56e6ac187 Eliminate edit echos for outgoing messages 2024-01-31 09:27:18 -05:00
Bohdan Horbeshko 20e6d2558e Version 1.9.0 2024-01-29 05:00:42 -05:00
Bohdan Horbeshko 3a60a1cfaa Bump Makefile to TDLib commit with the logout fix 2024-01-29 04:50:57 -05:00
Bohdan Horbeshko ea004b7f7c Reflect Telegram edits natively by nativeedits option 2024-01-29 04:28:15 -05:00
Bohdan Horbeshko c141c4ad2b Fix markable 2024-01-27 06:47:12 -05:00
Bohdan Horbeshko 599cf16cdb Request and send to Telegram XEP-0333 displayed markers by "receipts" option 2024-01-27 06:13:45 -05:00
Bohdan Horbeshko 81fc3ea370 Also ack with XEP-0184 read receipts for outgoing messages 2024-01-27 03:25:17 -05:00
Bohdan Horbeshko e37c428c67 XEP-0333 read markers for outgoing messages 2024-01-26 21:02:47 -05:00
Bohdan Horbeshko b9b6ba14a4 Fix stuck logout 2024-01-24 18:54:25 -05:00
Bohdan Horbeshko b40ccf4a4d Fix presences sent with no resource 2024-01-24 18:52:40 -05:00
Bohdan Horbeshko 4532748c84 Support chosen quotes in replies and replies from other chats 2024-01-10 14:30:00 -05:00
Bohdan Horbeshko f2807779aa Fix ending braces for PreCode 2023-11-16 08:44:26 -05:00
Bohdan Horbeshko 705cfc1d49 gofmt 2023-11-16 08:06:21 -05:00
Bohdan Horbeshko dcb802358b Fix tests 2023-11-16 08:05:23 -05:00
Bohdan Horbeshko 6bd8379114 Support blockquotes in formatter 2023-11-15 19:38:45 -05:00
Bohdan Horbeshko 576acba0d1 Migrate to TDLib 1.8.21 2023-11-11 16:10:23 -05:00
Bohdan Horbeshko 67b8ad57f0 Fix reply length for hrunicode messages 2023-10-29 08:52:33 -04:00
Bohdan Horbeshko 282a6fc21b Hotfix: prevent lockup on login 2023-08-31 18:24:30 -04:00
Bohdan Horbeshko 4588170d1e Harden the authorizer access to prevent crashes 2023-08-31 17:26:35 -04:00
Bohdan Horbeshko aa561c5be6 Version 1.8.0 2023-08-28 10:20:50 -04:00
22 changed files with 1327 additions and 459 deletions

1
.gitignore vendored
View file

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

View file

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

View file

@ -1,12 +1,13 @@
.PHONY: all test
COMMIT := $(shell git rev-parse --short HEAD)
TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1"
VERSION := "v1.8.0-dev"
TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551"
VERSION := "v1.9.6"
MAKEOPTS := "-j4"
all:
go build -ldflags "-X main.commit=${COMMIT}" -o telegabber
mkdir -p release
go build -ldflags "-X main.commit=${COMMIT}" -o release/telegabber
test:
go test -v ./config ./ ./telegram ./xmpp ./xmpp/gateway ./persistence ./telegram/formatter ./badger
@ -16,3 +17,9 @@ 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 .

2
go.mod
View file

@ -34,4 +34,4 @@ require (
)
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-20230730021136-47da33180615
replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061

4
go.sum
View file

@ -1,6 +1,10 @@
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=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

View file

@ -3,6 +3,7 @@ package persistence
import (
"github.com/pkg/errors"
"io/ioutil"
"sync"
"time"
"dev.narayana.im/narayana/telegabber/yamldb"
@ -34,14 +35,18 @@ type SessionsMap struct {
// Session is a key-values subtree
type Session struct {
Login string `yaml:":login"`
Timezone string `yaml:":timezone"`
KeepOnline bool `yaml:":keeponline"`
RawMessages bool `yaml:":rawmessages"`
AsciiArrows bool `yaml:":asciiarrows"`
OOBMode bool `yaml:":oobmode"`
Carbons bool `yaml:":carbons"`
HideIds bool `yaml:":hideids"`
Login string `yaml:":login"`
Timezone string `yaml:":timezone"`
KeepOnline bool `yaml:":keeponline"`
RawMessages bool `yaml:":rawmessages"`
AsciiArrows bool `yaml:":asciiarrows"`
OOBMode bool `yaml:":oobmode"`
Carbons bool `yaml:":carbons"`
HideIds bool `yaml:":hideids"`
Receipts bool `yaml:":receipts"`
NativeEdits bool `yaml:":nativeedits"`
IgnoredChats []int64 `yaml:":ignoredchats"`
ignoredChatsMap map[int64]bool `yaml:"-"`
}
var configKeys = []string{
@ -52,17 +57,26 @@ var configKeys = []string{
"oobmode",
"carbons",
"hideids",
"receipts",
"nativeedits",
}
var sessionDB *SessionsYamlDB
var sessionsLock sync.Mutex
// SessionMarshaller implementation for YamlDB
func SessionMarshaller() ([]byte, error) {
cleanedMap := SessionsMap{}
emptySessionsMap(&cleanedMap)
sessionsLock.Lock()
defer sessionsLock.Unlock()
for jid, session := range sessionDB.Data.Sessions {
if session.Login != "" {
session.IgnoredChats = make([]int64, 0, len(session.ignoredChatsMap))
for chatID := range session.ignoredChatsMap {
session.IgnoredChats = append(session.IgnoredChats, chatID)
}
cleanedMap.Sessions[jid] = session
}
}
@ -104,6 +118,16 @@ func initYamlDB(path string, dataPtr *SessionsMap) (*SessionsYamlDB, error) {
emptySessionsMap(dataPtr)
}
// convert ignored users slice to map
for jid, session := range dataPtr.Sessions {
session.ignoredChatsMap = make(map[int64]bool)
for _, chatID := range session.IgnoredChats {
session.ignoredChatsMap[chatID] = true
}
session.IgnoredChats = nil
dataPtr.Sessions[jid] = session
}
return &SessionsYamlDB{
YamlDB: yamldb.YamlDB{
Path: path,
@ -115,6 +139,13 @@ func initYamlDB(path string, dataPtr *SessionsMap) (*SessionsYamlDB, error) {
// Get retrieves a session value
func (s *Session) Get(key string) (string, error) {
sessionsLock.Lock()
defer sessionsLock.Unlock()
return s.get(key)
}
func (s *Session) get(key string) (string, error) {
switch key {
case "timezone":
return s.Timezone, nil
@ -130,6 +161,10 @@ func (s *Session) Get(key string) (string, error) {
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")
@ -137,9 +172,12 @@ func (s *Session) Get(key string) (string, error) {
// ToMap converts the session to a map
func (s *Session) ToMap() map[string]string {
sessionsLock.Lock()
defer sessionsLock.Unlock()
m := make(map[string]string)
for _, configKey := range configKeys {
value, _ := s.Get(configKey)
value, _ := s.get(configKey)
m[configKey] = value
}
@ -148,6 +186,9 @@ func (s *Session) ToMap() map[string]string {
// Set sets a session value
func (s *Session) Set(key string, value string) (string, error) {
sessionsLock.Lock()
defer sessionsLock.Unlock()
switch key {
case "timezone":
s.Timezone = value
@ -194,6 +235,20 @@ 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")
@ -210,6 +265,51 @@ func (s *Session) TimezoneToLocation() *time.Location {
return zeroLocation
}
// IgnoreChat adds a chat id to ignore list, returns false if already ignored
func (s *Session) IgnoreChat(chatID int64) bool {
sessionsLock.Lock()
defer sessionsLock.Unlock()
if s.ignoredChatsMap == nil {
s.ignoredChatsMap = make(map[int64]bool)
} else if _, ok := s.ignoredChatsMap[chatID]; ok {
return false
}
s.ignoredChatsMap[chatID] = true
return true
}
// UnignoreChat removes a chat id from ignore list, returns false if not already ignored
func (s *Session) UnignoreChat(chatID int64) bool {
sessionsLock.Lock()
defer sessionsLock.Unlock()
if s.ignoredChatsMap == nil {
return false
}
if _, ok := s.ignoredChatsMap[chatID]; !ok {
return false
}
delete(s.ignoredChatsMap, chatID)
return true
}
// IsChatIgnored checks the chat id against the ignore list
func (s *Session) IsChatIgnored(chatID int64) bool {
sessionsLock.Lock()
defer sessionsLock.Unlock()
if s.ignoredChatsMap == nil {
return false
}
_, ok := s.ignoredChatsMap[chatID]
return ok
}
func fromBool(b bool) string {
if b {
return "true"

View file

@ -48,6 +48,7 @@ func TestSessionToMap(t *testing.T) {
Timezone: "klsf",
RawMessages: true,
OOBMode: true,
Receipts: true,
}
m := session.ToMap()
sample := map[string]string{
@ -58,6 +59,8 @@ func TestSessionToMap(t *testing.T) {
"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)
@ -85,3 +88,31 @@ func TestSessionSetAbsent(t *testing.T) {
t.Error("There shouldn't come a donkey!")
}
}
func TestSessionIgnore(t *testing.T) {
session := Session{}
if session.IsChatIgnored(3) {
t.Error("Shouldn't be ignored yet")
}
if !session.IgnoreChat(3) {
t.Error("Shouldn't have been ignored")
}
if session.IgnoreChat(3) {
t.Error("Shouldn't ignore second time")
}
if !session.IsChatIgnored(3) {
t.Error("Should be ignored already")
}
if session.IsChatIgnored(-145) {
t.Error("Wrong chat is ignored")
}
if !session.UnignoreChat(3) {
t.Error("Should successfully unignore")
}
if session.UnignoreChat(3) {
t.Error("Should unignore second time")
}
if session.IsChatIgnored(3) {
t.Error("Shouldn't be ignored already")
}
}

46
staging.Dockerfile Normal file
View file

@ -0,0 +1,46 @@
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 /

23
tdlib.Dockerfile Normal file
View file

@ -0,0 +1,23 @@
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/ /

View file

@ -12,10 +12,11 @@ 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.8.0-dev"
var version string = "1.9.6"
var commit string
var sm *goxmpp.StreamManager
@ -60,6 +61,9 @@ 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)
@ -89,6 +93,25 @@ 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)

19
telegabber_test.go Normal file
View file

@ -0,0 +1,19 @@
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")
}
}

View file

@ -16,25 +16,6 @@ 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
@ -53,15 +34,18 @@ 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
locks clientLocks
@ -73,17 +57,18 @@ type clientLocks struct {
chatMessageLocks map[int64]*sync.Mutex
resourcesLock sync.Mutex
outboxLock sync.Mutex
editOutboxLock sync.Mutex
lastMsgHashesLock sync.Mutex
lastMsgIdsLock sync.RWMutex
authorizerReadLock sync.Mutex
authorizerWriteLock sync.Mutex
}
// NewClient instantiates a Telegram App
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,
@ -129,12 +114,14 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component
jid: jid,
Session: session,
resources: make(map[string]bool),
outbox: make(map[string]string),
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),

View file

@ -1,19 +0,0 @@
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")
}
}

View file

@ -85,8 +85,8 @@ var chatCommands = map[string]command{
"invite": command{"id or @username", "add user to current chat"},
"link": command{"", "get invite link for current chat"},
"kick": command{"id or @username", "remove user to current chat"},
"mute": command{"id or @username [hours]", "mute user in current chat"},
"unmute": command{"id or @username", "unrestrict user from current chat"},
"mute": command{"[id or @username] [hours]", "mute the whole chat or a user in current chat"},
"unmute": command{"[id or @username]", "unmute the whole chat or a user in the current chat"},
"ban": command{"id or @username [hours]", "restrict @username from current chat for [hours] or forever"},
"unban": command{"id or @username", "unbans @username in current chat (and devotes from admins)"},
"promote": command{"id or @username [title]", "promote user to admin in current chat"},
@ -185,18 +185,14 @@ func keyValueString(key, value string) string {
}
func (c *Client) unsubscribe(chatID int64) error {
return gateway.SendPresence(
c.xmpp,
c.jid,
gateway.SPFrom(strconv.FormatInt(chatID, 10)),
gateway.SPType("unsubscribed"),
)
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)
reply, _ := c.getMessageReply(message, false, true)
gateway.SendMessage(
c.jid,
@ -205,6 +201,8 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) {
strconv.FormatInt(message.Id, 10),
c.xmpp,
reply,
"",
false,
false,
)
}
@ -250,8 +248,14 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
return err.Error()
}
c.locks.authorizerWriteLock.Lock()
defer c.locks.authorizerWriteLock.Unlock()
c.authorizer.PhoneNumber <- args[0]
} else {
c.locks.authorizerWriteLock.Lock()
defer c.locks.authorizerWriteLock.Unlock()
if c.authorizer == nil {
return TelegramNotInitialized
}
@ -275,16 +279,15 @@ 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":
@ -324,10 +327,13 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
lastname = rawCmdArguments(cmdline, 1)
}
c.locks.authorizerWriteLock.Lock()
if c.authorizer != nil && !c.authorizer.isClosed {
c.authorizer.FirstName <- firstname
c.authorizer.LastName <- lastname
c.locks.authorizerWriteLock.Unlock()
} else {
c.locks.authorizerWriteLock.Unlock()
if !c.Online() {
return notOnline
}
@ -374,6 +380,7 @@ 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"
}
@ -384,7 +391,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
}
gateway.DirtySessions = true
return fmt.Sprintf("%s set to %s", args[0], value)
return fmt.Sprintf("%s%s set to %s", msg, args[0], value)
} else if len(args) > 0 {
value, err := c.Session.Get(args[0])
if err != nil {
@ -413,7 +420,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.ChatReportReasonCustom{},
Reason: &client.ReportReasonCustom{},
Text: text,
})
if err != nil {
@ -701,18 +708,18 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
}
// blacklists current user
case "block":
_, err := c.client.ToggleMessageSenderIsBlocked(&client.ToggleMessageSenderIsBlockedRequest{
_, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{
SenderId: &client.MessageSenderUser{UserId: chatID},
IsBlocked: true,
BlockList: &client.BlockListMain{},
})
if err != nil {
return err.Error(), true
}
// unblacklists current user
case "unblock":
_, err := c.client.ToggleMessageSenderIsBlocked(&client.ToggleMessageSenderIsBlockedRequest{
_, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{
SenderId: &client.MessageSenderUser{UserId: chatID},
IsBlocked: false,
BlockList: nil,
})
if err != nil {
return err.Error(), true
@ -764,59 +771,65 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
if err != nil {
return err.Error(), true
}
// mute @username [n hours]
// mute [@username [n hours]]
case "mute":
if len(args) < 1 {
return notEnoughArguments, true
}
contact, _, err := c.GetContactByUsername(args[0])
if err != nil {
return err.Error(), true
}
var hours int64
if len(args) > 1 {
hours, err = strconv.ParseInt(args[1], 10, 32)
if len(args) > 0 {
contact, _, err := c.GetContactByUsername(args[0])
if err != nil {
return "Invalid number of hours", true
return err.Error(), true
}
}
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &client.ChatMemberStatusRestricted{
IsMember: true,
RestrictedUntilDate: c.formatBantime(hours),
Permissions: &permissionsReadonly,
},
})
if err != nil {
return err.Error(), true
var hours int64
if len(args) > 1 {
hours, err = strconv.ParseInt(args[1], 10, 32)
if err != nil {
return "Invalid number of hours", true
}
}
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &client.ChatMemberStatusRestricted{
IsMember: true,
RestrictedUntilDate: c.formatBantime(hours),
Permissions: &permissionsReadonly,
},
})
if err != nil {
return err.Error(), true
}
} else {
if !c.Session.IgnoreChat(chatID) {
return "Chat is already ignored", true
}
gateway.DirtySessions = true
}
// unmute @username
// unmute [@username]
case "unmute":
if len(args) < 1 {
return notEnoughArguments, true
}
if len(args) > 0 {
contact, _, err := c.GetContactByUsername(args[0])
if err != nil {
return err.Error(), true
}
contact, _, err := c.GetContactByUsername(args[0])
if err != nil {
return err.Error(), true
}
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &client.ChatMemberStatusRestricted{
IsMember: true,
RestrictedUntilDate: 0,
Permissions: &permissionsMember,
},
})
if err != nil {
return err.Error(), true
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
ChatId: chatID,
MemberId: &client.MessageSenderUser{UserId: contact.Id},
Status: &client.ChatMemberStatusRestricted{
IsMember: true,
RestrictedUntilDate: 0,
Permissions: &permissionsMember,
},
})
if err != nil {
return err.Error(), true
}
} else {
if !c.Session.UnignoreChat(chatID) {
return "Chat wasn't ignored", true
}
gateway.DirtySessions = true
}
// ban @username from current chat [for N hours]
case "ban":

View file

@ -2,7 +2,6 @@ package telegram
import (
"github.com/pkg/errors"
"strconv"
"time"
"dev.narayana.im/narayana/telegabber/xmpp/gateway"
@ -69,10 +68,10 @@ func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.Auth
return nil
case client.TypeAuthorizationStateLoggingOut:
return client.ErrNotSupportedAuthorizationState
return nil
case client.TypeAuthorizationStateClosing:
return client.ErrNotSupportedAuthorizationState
return nil
case client.TypeAuthorizationStateClosed:
return client.ErrNotSupportedAuthorizationState
@ -110,6 +109,7 @@ func (c *Client) Connect(resource string) error {
log.Warn("Connecting to Telegram network...")
c.locks.authorizerWriteLock.Lock()
c.authorizer = &clientAuthorizer{
TdlibParameters: make(chan *client.SetTdlibParametersRequest, 1),
PhoneNumber: make(chan string, 1),
@ -121,8 +121,10 @@ func (c *Client) Connect(resource string) error {
}
go c.interactor()
log.Warn("Interactor launched")
c.authorizer.TdlibParameters <- c.parameters
c.locks.authorizerWriteLock.Unlock()
tdlibClient, err := client.NewClient(c.authorizer, c.options...)
if err != nil {
@ -156,7 +158,7 @@ func (c *Client) Connect(resource string) error {
}
gateway.SubscribeToTransport(c.xmpp, c.jid)
gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login))
c.sendPresence(gateway.SPStatus("Logged in as: " + c.Session.Login))
}()
return nil
@ -178,6 +180,9 @@ func (c *Client) TryLogin(resource string, login string) error {
time.Sleep(1e5)
}
c.locks.authorizerWriteLock.Lock()
defer c.locks.authorizerWriteLock.Unlock()
if c.authorizer == nil {
return errors.New(TelegramNotInitialized)
}
@ -190,6 +195,9 @@ func (c *Client) TryLogin(resource string, login string) error {
}
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")
}
@ -219,12 +227,8 @@ func (c *Client) Disconnect(resource string, quit bool) bool {
// we're offline (unsubscribe if logout)
for _, id := range c.cache.ChatsKeys() {
gateway.SendPresence(
c.xmpp,
c.jid,
gateway.SPFrom(strconv.FormatInt(id, 10)),
gateway.SPType("unavailable"),
)
args := gateway.SimplePresence(id, "unavailable")
c.sendPresence(args...)
}
c.close()
@ -234,9 +238,16 @@ func (c *Client) Disconnect(resource string, quit bool) bool {
func (c *Client) interactor() {
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
if !ok {
log.Warn("Interactor is disconnected")
c.locks.authorizerReadLock.Unlock()
return
}
@ -266,18 +277,27 @@ func (c *Client) interactor() {
log.Warn("Waiting for 2FA password...")
gateway.SendServiceMessage(c.jid, "Please, enter 2FA passphrase via /password 12345", c.xmpp)
}
c.locks.authorizerReadLock.Unlock()
}
}
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.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 {

View file

@ -8,15 +8,31 @@ import (
"github.com/zelenin/go-tdlib/client"
)
// Insertion is a piece of text in given position
type Insertion struct {
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 {
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("*")
@ -24,13 +40,18 @@ var italicRunes = []rune("_")
var strikeRunesMarkdown = []rune("~~")
var strikeRunesXEP0393 = []rune("~")
var codeRunes = []rune("`")
var preRuneStart = []rune("```\n")
var preRuneEnd = []rune("\n```")
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)
// 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]
@ -41,10 +62,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]
@ -120,21 +141,10 @@ func MergeAdjacentEntities(entities []*client.TextEntity) []*client.TextEntity {
}
// ClaspDirectives to the following span as required by XEP-0393
func ClaspDirectives(text string, entities []*client.TextEntity) []*client.TextEntity {
func ClaspDirectives(doubledRunes []rune, 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
@ -167,18 +177,89 @@ func ClaspDirectives(text string, entities []*client.TextEntity) []*client.TextE
return alignedEntities
}
func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) (*Insertion, *Insertion) {
return &Insertion{
func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) []*insertion {
return []*insertion{
&insertion{
Offset: entity.Offset,
Runes: lbrace,
}, &Insertion{
Type: insertionOpening,
},
&insertion{
Offset: entity.Offset + entity.Length,
Runes: rbrace,
}
Type: insertionClosing,
},
}
}
// EntityToMarkdown generates the wrapping Markdown tags
func EntityToMarkdown(entity *client.TextEntity) (*Insertion, *Insertion) {
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{}
}
switch entity.Type.TextEntityTypeType() {
case client.TypeTextEntityTypeBold:
return markupBraces(entity, boldRunesMarkdown, boldRunesMarkdown)
@ -189,22 +270,24 @@ func EntityToMarkdown(entity *client.TextEntity) (*Insertion, *Insertion) {
case client.TypeTextEntityTypeCode:
return markupBraces(entity, codeRunes, codeRunes)
case client.TypeTextEntityTypePre:
return markupBraces(entity, preRuneStart, preRuneEnd)
return markupBraces(entity, preRunesStart, preRunesEnd)
case client.TypeTextEntityTypePreCode:
preCode, _ := entity.Type.(*client.TextEntityTypePreCode)
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes)
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), preRunesEnd)
case client.TypeTextEntityTypeBlockQuote:
return quotePrependNewlines(entity, doubledRunes, MarkupModeMarkdown)
case client.TypeTextEntityTypeTextUrl:
textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl)
return markupBraces(entity, []rune("["), []rune("]("+textURL.Url+")"))
}
return nil, nil
return []*insertion{}
}
// EntityToXEP0393 generates the wrapping XEP-0393 tags
func EntityToXEP0393(entity *client.TextEntity) (*Insertion, *Insertion) {
// entityToXEP0393 generates the wrapping XEP-0393 tags
func entityToXEP0393(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion {
if entity == nil || entity.Type == nil {
return nil, nil
return []*insertion{}
}
switch entity.Type.TextEntityTypeType() {
@ -217,33 +300,59 @@ func EntityToXEP0393(entity *client.TextEntity) (*Insertion, *Insertion) {
case client.TypeTextEntityTypeCode:
return markupBraces(entity, codeRunes, codeRunes)
case client.TypeTextEntityTypePre:
return markupBraces(entity, preRuneStart, preRuneEnd)
return markupBraces(entity, preRunesStart, preRunesEnd)
case client.TypeTextEntityTypePreCode:
preCode, _ := entity.Type.(*client.TextEntityTypePreCode)
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes)
return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), preRunesEnd)
case client.TypeTextEntityTypeBlockQuote:
return quotePrependNewlines(entity, doubledRunes, MarkupModeXEP0393)
case client.TypeTextEntityTypeTextUrl:
textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl)
// non-standard, Pidgin-specific
return markupBraces(entity, []rune{}, []rune(" <"+textURL.Url+">"))
}
return nil, nil
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
}
// Format traverses an already sorted list of entities and wraps the text in a markup
func Format(
sourceText string,
entities []*client.TextEntity,
entityToMarkup func(*client.TextEntity) (*Insertion, *Insertion),
markupMode MarkupModeType,
) string {
if len(entities) == 0 {
return sourceText
}
mergedEntities := SortEntities(ClaspDirectives(sourceText, MergeAdjacentEntities(SortEntities(entities))))
var entityToMarkup func(*client.TextEntity, []rune, MarkupModeType) []*insertion
if markupMode == MarkupModeXEP0393 {
entityToMarkup = entityToXEP0393
} else {
entityToMarkup = entityToMarkdown
}
startStack := make(InsertionStack, 0, len(sourceText))
endStack := make(InsertionStack, 0, len(sourceText))
doubledRunes := textToDoubledRunes(sourceText)
mergedEntities := SortEntities(ClaspDirectives(doubledRunes, MergeAdjacentEntities(SortEntities(entities))))
startStack := make(insertionStack, 0, len(sourceText))
endStack := make(insertionStack, 0, len(sourceText))
// convert entities to a stack of brackets
var maxEndOffset int32
@ -260,36 +369,70 @@ func Format(
startStack, endStack = startStack.rebalance(endStack, entity.Offset)
startInsertion, endInsertion := entityToMarkup(entity)
if startInsertion != nil {
startStack = append(startStack, startInsertion)
insertions := entityToMarkup(entity, doubledRunes, markupMode)
if len(insertions) > 1 {
startStack = append(startStack, insertions[0:len(insertions)-1]...)
}
if endInsertion != nil {
endStack = append(endStack, endInsertion)
if len(insertions) > 0 {
endStack = append(endStack, insertions[len(insertions)-1])
}
}
// 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 runeI int32
var skipNext bool
for _, cp := range sourceText {
for insertion != nil && insertion.Offset <= runeI {
for i, cp := range doubledRunes {
if skipNext {
skipNext = false
continue
}
for insertion != nil && int(insertion.Offset) <= i {
markupRunes = append(markupRunes, insertion.Runes...)
insertion = nextInsertion()
}
markupRunes = append(markupRunes, cp)
// skip two UTF-16 code units (not points actually!) if needed
if cp > 0x0000ffff {
runeI += 2
} else {
runeI++
if cp > bmpCeil {
skipNext = true
}
}
for insertion != nil {

View file

@ -7,7 +7,7 @@ import (
)
func TestNoFormatting(t *testing.T) {
markup := Format("abc\ndef", []*client.TextEntity{}, EntityToMarkdown)
markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeMarkdown)
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{},
},
}, EntityToMarkdown)
}, MarkupModeMarkdown)
if markup != "👙**🐧🐖**" {
t.Errorf("Wrong simple formatting: %v", markup)
}
@ -40,7 +40,7 @@ func TestFormattingAdjacent(t *testing.T) {
Url: "https://narayana.im/",
},
},
}, EntityToMarkdown)
}, MarkupModeMarkdown)
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{},
},
}, EntityToMarkdown)
}, MarkupModeMarkdown)
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{}, EntityToXEP0393)
markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeXEP0393)
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{},
},
}, EntityToXEP0393)
}, MarkupModeXEP0393)
if markup != "👙*🐧🐖*" {
t.Errorf("Wrong simple formatting: %v", markup)
}
@ -241,7 +241,7 @@ func TestFormattingXEP0393Adjacent(t *testing.T) {
Url: "https://narayana.im/",
},
},
}, EntityToXEP0393)
}, MarkupModeXEP0393)
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{},
},
}, EntityToXEP0393)
}, MarkupModeXEP0393)
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{},
},
}, EntityToXEP0393)
}, MarkupModeXEP0393)
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{},
},
}, EntityToXEP0393)
}, MarkupModeXEP0393)
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{},
},
}, EntityToXEP0393)
}, MarkupModeXEP0393)
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{},
},
}, EntityToXEP0393)
}, MarkupModeXEP0393)
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{},
},
}, EntityToMarkdown)
}, MarkupModeMarkdown)
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{},
},
}, EntityToXEP0393)
}, MarkupModeXEP0393)
if markup != "Everyone ~dis~likes cake." {
t.Errorf("Wrong strikethrough formatting: %v", markup)
}
}
func TestClaspLeft(t *testing.T) {
text := "a b c"
text := textToDoubledRunes("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 := "a b c"
text := textToDoubledRunes("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 := " abc "
text := textToDoubledRunes(" abc ")
entities := []*client.TextEntity{
&client.TextEntity{
Offset: 1,
@ -439,7 +439,7 @@ func TestClaspNotNeeded(t *testing.T) {
}
func TestClaspNested(t *testing.T) {
text := "a b c"
text := textToDoubledRunes("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 := "a 🐖 c"
text := textToDoubledRunes("a 🐖 c")
entities := []*client.TextEntity{
&client.TextEntity{
Offset: 1,
@ -472,3 +472,111 @@ 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)
}
}

View file

@ -55,6 +55,31 @@ 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()
@ -141,6 +166,12 @@ 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
@ -204,6 +235,9 @@ func (c *Client) updateChatLastMessage(update *client.UpdateChatLastMessage) {
// message received
func (c *Client) updateNewMessage(update *client.UpdateNewMessage) {
chatId := update.Message.ChatId
if c.Session.IsChatIgnored(chatId) {
return
}
// guarantee sequential message delivering per chat
lock := c.getChatMessageLock(chatId)
@ -211,6 +245,8 @@ 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 &&
@ -223,23 +259,31 @@ 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)
}()
}
// message content updated
func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
if c.Session.IsChatIgnored(update.ChatId) {
return
}
markupFunction := c.getFormatter()
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, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId)
xmppId, xmppIdErr := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId)
var ignoredResource string
if err == nil {
ignoredResource = c.popFromOutbox(xmppId)
if xmppIdErr == nil {
ignoredResource = c.popFromEditOutbox(xmppId)
} else {
log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.MessageId)
}
@ -253,19 +297,62 @@ 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)
var editChar string
if c.Session.AsciiArrows {
editChar = "e "
} else {
editChar = "✎ "
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)
}
}
text := editChar + fmt.Sprintf("%v | %s", update.MessageId, formatter.Format(
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)
} 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))
}
text.WriteString(formatter.Format(
textContent.Text.Text,
textContent.Text.Entities,
markupFunction,
))
sChatId := strconv.FormatInt(update.ChatId, 10)
for _, jid := range jids {
gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, false)
gateway.SendMessage(jid, sChatId, text.String(), "e"+sId, c.xmpp, nil, replaceId, isCarbon, false)
}
}
}
@ -273,6 +360,10 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
// message(s) deleted
func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) {
if update.IsPermanent {
if c.Session.IsChatIgnored(update.ChatId) {
return
}
var deleteChar string
if c.Session.AsciiArrows {
deleteChar = "X "
@ -294,19 +385,25 @@ 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)
@ -328,3 +425,7 @@ 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)
}

View file

@ -16,6 +16,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"
"dev.narayana.im/narayana/telegabber/telegram/cache"
"dev.narayana.im/narayana/telegabber/telegram/formatter"
@ -36,13 +37,21 @@ type VCardInfo struct {
Info string
}
type messageStub struct {
MessageId int64
ChatId int64
Sender string
Date int32
Text string
}
var errOffline = errors.New("TDlib instance is offline")
var spaceRegex = regexp.MustCompile(`\s+`)
var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n")
const newlineChar string = "\n"
const messageHeaderSeparator string = " | "
const messageHeaderSeparator string = " | " // no hrunicode allowed here yet
// GetContactByUsername resolves username to user id retrieves user and chat information
func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) {
@ -249,15 +258,15 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
presenceType = typ
}
log.WithFields(log.Fields{
"show": show,
"status": status,
"show": show,
"status": status,
"presenceType": presenceType,
}).Debug("Cached status")
} else if user != nil && user.Status != nil {
show, status, presenceType = c.userStatusToText(user.Status, chatID)
log.WithFields(log.Fields{
"show": show,
"status": status,
"show": show,
"status": status,
"presenceType": presenceType,
}).Debug("Status to text")
} else {
@ -272,22 +281,17 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
c.cache.SetStatus(chatID, cacheShow, status)
newArgs := []args.V{
gateway.SPFrom(strconv.FormatInt(chatID, 10)),
gateway.SPShow(show),
gateway.SPStatus(status),
gateway.SPPhoto(photo),
gateway.SPResource(gateway.Jid.Resource),
gateway.SPImmed(gateway.SPImmed.Get(oldArgs)),
}
newArgs = gateway.SPAppendFrom(newArgs, chatID)
if presenceType != "" {
newArgs = append(newArgs, gateway.SPType(presenceType))
}
return gateway.SendPresence(
c.xmpp,
c.jid,
newArgs...,
)
return c.sendPresence(newArgs...)
}
func (c *Client) formatContact(chatID int64) string {
@ -341,25 +345,74 @@ 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
}
func (c *Client) messageToStub(message *client.Message, preview bool, text string) *messageStub {
if text == "" {
text = c.messageContentToText(message.Content, message.ChatId, preview)
}
return &messageStub{
MessageId: message.Id,
ChatId: message.ChatId,
Sender: c.formatSender(message),
Date: message.Date,
Text: text,
}
}
replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, message.ReplyToMessageId)
if err != nil {
replyId = strconv.FormatInt(message.ReplyToMessageId, 10)
func (c *Client) getMessageReply(message *client.Message, preview bool, noContent bool) (gatewayReply *gateway.Reply, tgReply *messageStub) {
if message.ReplyTo != nil && message.ReplyTo.MessageReplyToType() == client.TypeMessageReplyToMessage {
replyTo, _ := message.ReplyTo.(*client.MessageReplyToMessage)
var text string
if replyTo.Quote != nil && replyTo.Quote.Text != nil && !noContent {
text = formatter.Format(
replyTo.Quote.Text.Text,
replyTo.Quote.Text.Entities,
c.getFormatter(),
)
// make the whole quote fit one line
text = strings.ReplaceAll(text, "\n", " ")
}
reply = &gateway.Reply{
Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()),
Id: replyId,
if message.ChatId == replyTo.ChatId {
// obtain message from this chat
replyMsg, err := c.client.GetMessage(&client.GetMessageRequest{
ChatId: message.ChatId,
MessageId: replyTo.MessageId,
})
if err != nil {
log.Errorf("<error fetching message: %s>", err.Error())
return
}
if !noContent {
tgReply = c.messageToStub(replyMsg, preview, text)
}
replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, replyTo.MessageId)
if err != nil {
replyId = strconv.FormatInt(replyTo.MessageId, 10)
}
gatewayReply = &gateway.Reply{
Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()),
Id: replyId,
}
} else if !noContent {
// it's safe to assume there's no need to pass ChatId here
// as it's needed only for pin messages which are not allowed in replies
if text == "" && replyTo.Content != nil {
text = c.messageContentToText(replyTo.Content, 0, preview)
}
if text == "" {
log.Error("Empty reply from other/unknown chat")
log.Debugf("replyTo: %#v", replyTo)
return
}
tgReply = &messageStub{
Sender: c.formatOrigin(replyTo.Origin) + " @ " + c.formatContact(replyTo.ChatId),
Date: replyTo.OriginSendDate,
Text: text,
}
}
}
@ -382,9 +435,16 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess
return ""
}
return c.formatMessageContent(preview, c.messageToStub(message, preview, ""))
}
func (c *Client) formatMessageContent(preview bool, message *messageStub) string {
var str strings.Builder
// add messageid and sender
str.WriteString(fmt.Sprintf("%v | %s | ", message.Id, c.formatSender(message)))
if message.MessageId != 0 {
str.WriteString(fmt.Sprintf("%v | ", message.MessageId))
}
str.WriteString(fmt.Sprintf("%s | ", message.Sender))
// add date
if !preview {
str.WriteString(
@ -395,10 +455,7 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess
}
// text message
var text string
if message.Content != nil {
text = c.messageToText(message, preview)
}
text := message.Text
if text != "" {
if !preview {
str.WriteString(text)
@ -415,33 +472,33 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess
return str.String()
}
func (c *Client) formatForward(fwd *client.MessageForwardInfo) string {
switch fwd.Origin.MessageForwardOriginType() {
case client.TypeMessageForwardOriginUser:
originUser := fwd.Origin.(*client.MessageForwardOriginUser)
func (c *Client) formatOrigin(origin client.MessageOrigin) string {
if origin == nil {
return ""
}
switch origin.MessageOriginType() {
case client.TypeMessageOriginUser:
originUser := origin.(*client.MessageOriginUser)
return c.formatContact(originUser.SenderUserId)
case client.TypeMessageForwardOriginChat:
originChat := fwd.Origin.(*client.MessageForwardOriginChat)
case client.TypeMessageOriginChat:
originChat := origin.(*client.MessageOriginChat)
var signature string
if originChat.AuthorSignature != "" {
signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature)
}
return c.formatContact(originChat.SenderChatId) + signature
case client.TypeMessageForwardOriginHiddenUser:
originUser := fwd.Origin.(*client.MessageForwardOriginHiddenUser)
case client.TypeMessageOriginHiddenUser:
originUser := origin.(*client.MessageOriginHiddenUser)
return originUser.SenderName
case client.TypeMessageForwardOriginChannel:
channel := fwd.Origin.(*client.MessageForwardOriginChannel)
case client.TypeMessageOriginChannel:
channel := origin.(*client.MessageOriginChannel)
var signature string
if channel.AuthorSignature != "" {
signature = fmt.Sprintf(" (%s)", channel.AuthorSignature)
}
return c.formatContact(channel.ChatId) + signature
case client.TypeMessageForwardOriginMessageImport:
originImport := fwd.Origin.(*client.MessageForwardOriginMessageImport)
return originImport.SenderName
}
return "Unknown forward type"
return "Unknown origin type"
}
func (c *Client) formatFile(file *client.File, compact bool) (string, string) {
@ -587,20 +644,24 @@ func (c *Client) messageToText(message *client.Message, preview bool) string {
return "<empty message>"
}
markupFunction := c.getFormatter()
switch message.Content.MessageContentType() {
return c.messageContentToText(message.Content, message.ChatId, preview)
}
func (c *Client) messageContentToText(content client.MessageContent, chatId int64, preview bool) string {
markupMode := c.getFormatter()
switch content.MessageContentType() {
case client.TypeMessageSticker:
sticker, _ := message.Content.(*client.MessageSticker)
sticker, _ := content.(*client.MessageSticker)
return sticker.Sticker.Emoji
case client.TypeMessageAnimatedEmoji:
animatedEmoji, _ := message.Content.(*client.MessageAnimatedEmoji)
animatedEmoji, _ := content.(*client.MessageAnimatedEmoji)
return animatedEmoji.Emoji
case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate:
return "has created chat"
case client.TypeMessageChatJoinByLink:
return "joined chat via invite link"
case client.TypeMessageChatAddMembers:
addMembers, _ := message.Content.(*client.MessageChatAddMembers)
addMembers, _ := content.(*client.MessageChatAddMembers)
text := "invited "
if len(addMembers.MemberUserIds) > 0 {
@ -609,19 +670,19 @@ func (c *Client) messageToText(message *client.Message, preview bool) string {
return text
case client.TypeMessageChatDeleteMember:
deleteMember, _ := message.Content.(*client.MessageChatDeleteMember)
deleteMember, _ := content.(*client.MessageChatDeleteMember)
return "kicked " + c.formatContact(deleteMember.UserId)
case client.TypeMessagePinMessage:
pinMessage, _ := message.Content.(*client.MessagePinMessage)
return "pinned message: " + c.formatMessage(message.ChatId, pinMessage.MessageId, preview, nil)
pinMessage, _ := content.(*client.MessagePinMessage)
return "pinned message: " + c.formatMessage(chatId, pinMessage.MessageId, preview, nil)
case client.TypeMessageChatChangeTitle:
changeTitle, _ := message.Content.(*client.MessageChatChangeTitle)
changeTitle, _ := content.(*client.MessageChatChangeTitle)
return "chat title set to: " + changeTitle.Title
case client.TypeMessageLocation:
location, _ := message.Content.(*client.MessageLocation)
location, _ := content.(*client.MessageLocation)
return c.formatLocation(location.Location)
case client.TypeMessageVenue:
venue, _ := message.Content.(*client.MessageVenue)
venue, _ := content.(*client.MessageVenue)
if preview {
return venue.Venue.Title
} else {
@ -633,86 +694,86 @@ func (c *Client) messageToText(message *client.Message, preview bool) string {
)
}
case client.TypeMessagePhoto:
photo, _ := message.Content.(*client.MessagePhoto)
photo, _ := content.(*client.MessagePhoto)
if preview {
return photo.Caption.Text
} else {
return formatter.Format(
photo.Caption.Text,
photo.Caption.Entities,
markupFunction,
markupMode,
)
}
case client.TypeMessageAudio:
audio, _ := message.Content.(*client.MessageAudio)
audio, _ := content.(*client.MessageAudio)
if preview {
return audio.Caption.Text
} else {
return formatter.Format(
audio.Caption.Text,
audio.Caption.Entities,
markupFunction,
markupMode,
)
}
case client.TypeMessageVideo:
video, _ := message.Content.(*client.MessageVideo)
video, _ := content.(*client.MessageVideo)
if preview {
return video.Caption.Text
} else {
return formatter.Format(
video.Caption.Text,
video.Caption.Entities,
markupFunction,
markupMode,
)
}
case client.TypeMessageDocument:
document, _ := message.Content.(*client.MessageDocument)
document, _ := content.(*client.MessageDocument)
if preview {
return document.Caption.Text
} else {
return formatter.Format(
document.Caption.Text,
document.Caption.Entities,
markupFunction,
markupMode,
)
}
case client.TypeMessageText:
text, _ := message.Content.(*client.MessageText)
text, _ := content.(*client.MessageText)
if preview {
return text.Text.Text
} else {
return formatter.Format(
text.Text.Text,
text.Text.Entities,
markupFunction,
markupMode,
)
}
case client.TypeMessageVoiceNote:
voice, _ := message.Content.(*client.MessageVoiceNote)
voice, _ := content.(*client.MessageVoiceNote)
if preview {
return voice.Caption.Text
} else {
return formatter.Format(
voice.Caption.Text,
voice.Caption.Entities,
markupFunction,
markupMode,
)
}
case client.TypeMessageVideoNote:
return ""
case client.TypeMessageAnimation:
animation, _ := message.Content.(*client.MessageAnimation)
animation, _ := content.(*client.MessageAnimation)
if preview {
return animation.Caption.Text
} else {
return formatter.Format(
animation.Caption.Text,
animation.Caption.Entities,
markupFunction,
markupMode,
)
}
case client.TypeMessageContact:
contact, _ := message.Content.(*client.MessageContact)
contact, _ := content.(*client.MessageContact)
if preview {
return contact.Contact.FirstName + " " + contact.Contact.LastName
} else {
@ -730,10 +791,10 @@ func (c *Client) messageToText(message *client.Message, preview bool) string {
)
}
case client.TypeMessageDice:
dice, _ := message.Content.(*client.MessageDice)
dice, _ := content.(*client.MessageDice)
return fmt.Sprintf("%s 1d6: [%v]", dice.Emoji, dice.Value)
case client.TypeMessagePoll:
poll, _ := message.Content.(*client.MessagePoll)
poll, _ := content.(*client.MessagePoll)
if preview {
return poll.Poll.Question
@ -759,7 +820,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string {
return strings.Join(rows, "\n")
}
case client.TypeMessageChatSetMessageAutoDeleteTime:
ttl, _ := message.Content.(*client.MessageChatSetMessageAutoDeleteTime)
ttl, _ := content.(*client.MessageChatSetMessageAutoDeleteTime)
name := c.formatContact(ttl.FromUserId)
if name == "" {
if ttl.MessageAutoDeleteTime == 0 {
@ -776,7 +837,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string {
}
}
return fmt.Sprintf("unknown message (%s)", message.Content.MessageContentType())
return fmt.Sprintf("unknown message (%s)", content.MessageContentType())
}
func (c *Client) contentToFile(content client.MessageContent) (*client.File, *client.File) {
@ -845,21 +906,23 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl
func (c *Client) countCharsInLines(lines *[]string) (count int) {
for _, line := range *lines {
count += len(line)
count += utf8.RuneCountInString(line)
}
return
}
func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, replyMsg *client.Message) (string, int, int) {
func (c *Client) isCarbonsEnabled() bool {
return gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons
}
func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, suppressReply bool) (string, *gateway.Reply) {
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)
hideSender := c.isCarbonsEnabled() && (message.IsOutgoing || isPM)
var replyStart, replyEnd int
prefix := []string{}
// message direction
var directionChar string
@ -888,20 +951,39 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string,
prefix = append(prefix, sender)
}
}
// reply to
if message.ReplyToMessageId != 0 {
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)
var reply *gateway.Reply
if !suppressReply {
preview := true
gwReply, tgReply := c.getMessageReply(message, preview, false)
if tgReply != nil {
reply = gwReply
var replyStart, replyEnd int
if len(prefix) > 0 {
replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator)
}
replyLine := "reply: " + c.formatMessageContent(preview, tgReply)
prefix = append(prefix, replyLine)
replyEnd = replyStart + utf8.RuneCountInString(replyLine)
if len(prefix) > 0 {
replyEnd += len(messageHeaderSeparator)
}
if reply != nil {
reply.Start = uint64(replyStart)
reply.End = uint64(replyEnd)
}
}
}
if message.ForwardInfo != nil {
prefix = append(prefix, "fwd: "+c.formatForward(message.ForwardInfo))
prefix = append(prefix, "fwd: "+c.formatOrigin(message.ForwardInfo.Origin))
}
// preview
if previewString != "" {
@ -912,7 +994,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string,
prefix = append(prefix, "file: "+fileString)
}
return strings.Join(prefix, messageHeaderSeparator), replyStart, replyEnd
return strings.Join(prefix, messageHeaderSeparator), reply
}
func (c *Client) ensureDownloadFile(file *client.File) *client.File {
@ -931,14 +1013,25 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.File {
return file
}
// \n if it is groupchat and message is not empty
func (c *Client) getPrefixSeparator(chatId int64) string {
var separator string
if chatId < 0 {
separator = "\n"
} else if chatId > 0 {
separator = " | "
}
return separator
}
// ProcessIncomingMessage transfers a message to XMPP side and marks it as read on Telegram side
func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
isCarbon := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons && message.IsOutgoing
isCarbon := c.isCarbonsEnabled() && message.IsOutgoing
jids := c.getCarbonFullJids(isCarbon, "")
var text, oob, auxText string
reply, replyMsg := c.getMessageReply(message)
var reply *gateway.Reply
var replyObtained bool
content := message.Content
if content != nil && content.MessageContentType() == client.TypeMessageChatChangePhoto {
@ -965,56 +1058,72 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
fileName, link := c.formatFile(file, false)
oob = link
if c.Session.OOBMode && oob != "" {
typ := message.Content.MessageContentType()
if typ != client.TypeMessageSticker {
auxText = text
oobSwap := c.Session.OOBMode && oob != ""
var ignorePrefix bool
if oobSwap {
if text == "" || message.Content.MessageContentType() == client.TypeMessageSticker {
isPM, err := c.IsPM(chatId)
if err == nil {
ignorePrefix = isPM && c.isCarbonsEnabled()
}
}
text = oob
} else if !c.Session.RawMessages {
}
if !c.Session.RawMessages && !ignorePrefix {
var newText strings.Builder
prefix, replyStart, replyEnd := c.messageToPrefix(message, previewName, fileName, replyMsg)
prefix, prefixReply := c.messageToPrefix(message, previewName, fileName, false)
reply = prefixReply
replyObtained = true
newText.WriteString(prefix)
if reply != nil {
reply.Start = uint64(replyStart)
reply.End = uint64(replyEnd)
}
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(" | ")
}
newText.WriteString(c.getPrefixSeparator(chatId))
}
newText.WriteString(text)
}
text = newText.String()
}
if oobSwap {
if !ignorePrefix {
auxText = text
}
text = oob
}
}
}
if !replyObtained {
reply, _ = c.getMessageReply(message, false, true)
}
// mark message as read
c.client.ViewMessages(&client.ViewMessagesRequest{
ChatId: chatId,
MessageIds: []int64{message.Id},
ForceRead: true,
})
if !c.Session.Receipts {
c.MarkAsRead(chatId, message.Id)
}
// forward message to 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)
gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, "", isCarbon, c.Session.Receipts)
if auxText != "" {
gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isCarbon)
gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, "", isCarbon, c.Session.Receipts)
}
}
c.UpdateLastChatMessageId(chatId, sId)
}
// MarkAsRead marks a message as read
func (c *Client) MarkAsRead(chatId, messageId int64) {
c.client.ViewMessages(&client.ViewMessagesRequest{
ChatId: chatId,
MessageIds: []int64{messageId},
ForceRead: true,
})
}
// PrepareMessageContent creates a simple text message
@ -1115,7 +1224,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str
tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{
ChatId: chatID,
ReplyToMessageId: reply,
ReplyTo: &client.InputMessageReplyToMessage{MessageId: reply},
InputMessageContent: content,
})
if err != nil {
@ -1212,7 +1321,7 @@ func (c *Client) roster(resource string) {
c.ProcessStatusUpdate(chat, "", "")
}
gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login))
c.sendPresence(gateway.SPStatus("Logged in as: " + c.Session.Login))
c.addResource(resource)
}
@ -1313,9 +1422,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string {
// subscribe to a Telegram ID
func (c *Client) subscribeToID(id int64, chat *client.Chat) {
var args []args.V
args = append(args, gateway.SPFrom(strconv.FormatInt(id, 10)))
args = append(args, gateway.SPType("subscribe"))
args := gateway.SimplePresence(id, "subscribe")
if chat == nil {
chat, _, _ = c.GetContactByID(id, nil)
@ -1326,11 +1433,11 @@ func (c *Client) subscribeToID(id int64, chat *client.Chat) {
gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp)
}
gateway.SendPresence(
c.xmpp,
c.jid,
args...,
)
c.sendPresence(args...)
}
func (c *Client) sendPresence(args ...args.V) error {
return gateway.SendPresence(c.xmpp, c.jid, args...)
}
func (c *Client) prepareDiskSpace(size uint64) {
@ -1379,9 +1486,9 @@ func (c *Client) UpdateChatNicknames() {
chat, ok := c.cache.GetChat(id)
if ok {
newArgs := []args.V{
gateway.SPFrom(strconv.FormatInt(id, 10)),
gateway.SPNickname(chat.Title),
}
newArgs = gateway.SPAppendFrom(newArgs, id)
cachedStatus, ok := c.cache.GetStatus(id)
if ok {
@ -1392,17 +1499,34 @@ func (c *Client) UpdateChatNicknames() {
}
}
gateway.SendPresence(
c.xmpp,
c.jid,
newArgs...,
)
c.sendPresence(newArgs...)
gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp)
}
}
}
// AddToEditOutbox temporarily store the resource from which a replace message with given ID was sent
func (c *Client) AddToEditOutbox(xmppId, resource string) {
c.locks.editOutboxLock.Lock()
defer c.locks.editOutboxLock.Unlock()
c.editOutbox[xmppId] = resource
}
func (c *Client) popFromEditOutbox(xmppId string) string {
c.locks.editOutboxLock.Lock()
defer c.locks.editOutboxLock.Unlock()
resource, ok := c.editOutbox[xmppId]
if ok {
delete(c.editOutbox, xmppId)
} else {
log.Warnf("No %v xmppId in edit outbox", xmppId)
}
return resource
}
// AddToOutbox remembers the resource from which a message with given ID was sent
func (c *Client) AddToOutbox(xmppId, resource string) {
c.locks.outboxLock.Lock()
@ -1411,14 +1535,12 @@ func (c *Client) AddToOutbox(xmppId, resource string) {
c.outbox[xmppId] = resource
}
func (c *Client) popFromOutbox(xmppId string) string {
func (c *Client) getFromOutbox(xmppId string) string {
c.locks.outboxLock.Lock()
defer c.locks.outboxLock.Unlock()
resource, ok := c.outbox[xmppId]
if ok {
delete(c.outbox, xmppId)
} else {
if !ok {
log.Warnf("No %v xmppId in outbox", xmppId)
}
return resource
@ -1493,8 +1615,23 @@ func (c *Client) hasLastMessageHashChanged(chatId, messageId int64, content clie
return !ok || oldHash != newHash
}
func (c *Client) getFormatter() func(*client.TextEntity) (*formatter.Insertion, *formatter.Insertion) {
return formatter.EntityToXEP0393
func (c *Client) UpdateLastChatMessageId(chatId int64, messageId string) {
c.locks.lastMsgIdsLock.Lock()
defer c.locks.lastMsgIdsLock.Unlock()
c.lastMsgIds[chatId] = messageId
}
func (c *Client) getLastChatMessageId(chatId int64) (string, bool) {
c.locks.lastMsgIdsLock.RLock()
defer c.locks.lastMsgIdsLock.RUnlock()
xmppId, ok := c.lastMsgIds[chatId]
return xmppId, ok
}
func (c *Client) getFormatter() formatter.MarkupModeType {
return formatter.MarkupModeXEP0393
}
func (c *Client) usernamesToString(usernames []string) string {

View file

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

View file

@ -3,12 +3,14 @@ package gateway
import (
"encoding/xml"
"github.com/pkg/errors"
"strconv"
"strings"
"sync"
"dev.narayana.im/narayana/telegabber/badger"
"dev.narayana.im/narayana/telegabber/xmpp/extensions"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/soheilhy/args"
"gosrc.io/xmpp"
@ -22,6 +24,18 @@ 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
@ -42,26 +56,42 @@ 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, isCarbon bool) {
sendMessageWrapper(to, from, body, id, component, reply, "", isCarbon)
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)
}
// 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)
var id string
if uuid, err := uuid.NewRandom(); err == nil {
id = uuid.String()
}
sendMessageWrapper(to, "", body, id, component, nil, nil, "", "", false, 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)
var id string
if uuid, err := uuid.NewRandom(); err == nil {
id = uuid.String()
}
sendMessageWrapper(to, from, body, id, component, nil, nil, "", "", false, 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 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 sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) {
// 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)
}
func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, marker *marker, oob, replaceId string, isCarbon, requestReceipt bool) {
toJid, err := stanza.NewJid(to)
if err != nil {
log.WithFields(log.Fields{
@ -119,9 +149,23 @@ 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 != "" {
message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
}
if requestReceipt {
message.Extensions = append(message.Extensions, stanza.Markable{})
}
if replaceId != "" {
message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId})
}
if isCarbon {
carbonMessage := extensions.ClientMessage{
@ -343,6 +387,20 @@ 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)

View file

@ -199,10 +199,13 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
if err != nil {
log.Errorf("Failed to replace id %v with %v %v", replace.Id, msg.Id, tgMessageId)
} */
session.AddToOutbox(replace.Id, resource)
session.AddToEditOutbox(replace.Id, resource)
} else {
err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id)
if err != nil {
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)
}
}
@ -252,6 +255,30 @@ 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" {
@ -458,6 +485,8 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
_, 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")
@ -624,35 +653,35 @@ func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code
switch code {
case 400:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeModify,
Code: code,
Type: stanza.ErrorTypeModify,
Reason: "bad-request",
}
case 405:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeCancel,
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",
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,
Code: code,
Type: stanza.ErrorTypeModify,
Reason: "not-acceptable",
Text: "Phone number already provided, chat with the transport for further instruction",
Text: "Phone number already provided, chat with the transport for further instruction",
}
case 500:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeWait,
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,
Code: code,
Type: stanza.ErrorTypeCancel,
Reason: "undefined-condition",
}
}