Compare commits

..

29 commits
muc ... 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
23 changed files with 1404 additions and 1488 deletions

1
.gitignore vendored
View file

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

View file

@ -29,7 +29,7 @@ WORKDIR /src
RUN make ${MAKEOPTS} RUN make ${MAKEOPTS}
FROM scratch AS telegabber 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"] ENTRYPOINT ["/usr/local/bin/telegabber"]
FROM scratch AS binaries FROM scratch AS binaries

View file

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

4
go.mod
View file

@ -33,5 +33,5 @@ require (
nhooyr.io/websocket v1.6.5 // indirect nhooyr.io/websocket v1.6.5 // indirect
) )
replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-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

8
go.sum
View file

@ -1,12 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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 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-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 h1:6249ajbMjgYz53Oq0IjTvjHXbxTfu29Mj1J/6swRHs4=
dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f h1:aT50UsPH1dLje9CCAquRRhr7I9ZvL3kQU6WIWTe8PZ0=
dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 h1:GbV1Lv3lVHsSeKAqPTBem72OCsGjXntW4jfJdXciE+w=
github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7/go.mod h1:ZzkRfuaFj8etIYMj/ECtXtgfz72RE6U+dos27b3XIwk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=

View file

@ -3,6 +3,7 @@ package persistence
import ( import (
"github.com/pkg/errors" "github.com/pkg/errors"
"io/ioutil" "io/ioutil"
"sync"
"time" "time"
"dev.narayana.im/narayana/telegabber/yamldb" "dev.narayana.im/narayana/telegabber/yamldb"
@ -34,15 +35,18 @@ type SessionsMap struct {
// Session is a key-values subtree // Session is a key-values subtree
type Session struct { type Session struct {
Login string `yaml:":login"` Login string `yaml:":login"`
Timezone string `yaml:":timezone"` Timezone string `yaml:":timezone"`
KeepOnline bool `yaml:":keeponline"` KeepOnline bool `yaml:":keeponline"`
RawMessages bool `yaml:":rawmessages"` RawMessages bool `yaml:":rawmessages"`
AsciiArrows bool `yaml:":asciiarrows"` AsciiArrows bool `yaml:":asciiarrows"`
MUC bool `yaml:":muc"` OOBMode bool `yaml:":oobmode"`
OOBMode bool `yaml:":oobmode"` Carbons bool `yaml:":carbons"`
Carbons bool `yaml:":carbons"` HideIds bool `yaml:":hideids"`
HideIds bool `yaml:":hideids"` Receipts bool `yaml:":receipts"`
NativeEdits bool `yaml:":nativeedits"`
IgnoredChats []int64 `yaml:":ignoredchats"`
ignoredChatsMap map[int64]bool `yaml:"-"`
} }
var configKeys = []string{ var configKeys = []string{
@ -50,21 +54,29 @@ var configKeys = []string{
"keeponline", "keeponline",
"rawmessages", "rawmessages",
"asciiarrows", "asciiarrows",
"muc",
"oobmode", "oobmode",
"carbons", "carbons",
"hideids", "hideids",
"receipts",
"nativeedits",
} }
var sessionDB *SessionsYamlDB var sessionDB *SessionsYamlDB
var sessionsLock sync.Mutex
// SessionMarshaller implementation for YamlDB // SessionMarshaller implementation for YamlDB
func SessionMarshaller() ([]byte, error) { func SessionMarshaller() ([]byte, error) {
cleanedMap := SessionsMap{} cleanedMap := SessionsMap{}
emptySessionsMap(&cleanedMap) emptySessionsMap(&cleanedMap)
sessionsLock.Lock()
defer sessionsLock.Unlock()
for jid, session := range sessionDB.Data.Sessions { for jid, session := range sessionDB.Data.Sessions {
if session.Login != "" { 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 cleanedMap.Sessions[jid] = session
} }
} }
@ -106,6 +118,16 @@ func initYamlDB(path string, dataPtr *SessionsMap) (*SessionsYamlDB, error) {
emptySessionsMap(dataPtr) 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{ return &SessionsYamlDB{
YamlDB: yamldb.YamlDB{ YamlDB: yamldb.YamlDB{
Path: path, Path: path,
@ -117,6 +139,13 @@ func initYamlDB(path string, dataPtr *SessionsMap) (*SessionsYamlDB, error) {
// Get retrieves a session value // Get retrieves a session value
func (s *Session) Get(key string) (string, error) { 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 { switch key {
case "timezone": case "timezone":
return s.Timezone, nil return s.Timezone, nil
@ -126,14 +155,16 @@ func (s *Session) Get(key string) (string, error) {
return fromBool(s.RawMessages), nil return fromBool(s.RawMessages), nil
case "asciiarrows": case "asciiarrows":
return fromBool(s.AsciiArrows), nil return fromBool(s.AsciiArrows), nil
case "muc":
return fromBool(s.MUC), nil
case "oobmode": case "oobmode":
return fromBool(s.OOBMode), nil return fromBool(s.OOBMode), nil
case "carbons": case "carbons":
return fromBool(s.Carbons), nil return fromBool(s.Carbons), nil
case "hideids": case "hideids":
return fromBool(s.HideIds), nil 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") return "", errors.New("Unknown session property")
@ -141,9 +172,12 @@ func (s *Session) Get(key string) (string, error) {
// ToMap converts the session to a map // ToMap converts the session to a map
func (s *Session) ToMap() map[string]string { func (s *Session) ToMap() map[string]string {
sessionsLock.Lock()
defer sessionsLock.Unlock()
m := make(map[string]string) m := make(map[string]string)
for _, configKey := range configKeys { for _, configKey := range configKeys {
value, _ := s.Get(configKey) value, _ := s.get(configKey)
m[configKey] = value m[configKey] = value
} }
@ -152,6 +186,9 @@ func (s *Session) ToMap() map[string]string {
// Set sets a session value // Set sets a session value
func (s *Session) Set(key string, value string) (string, error) { func (s *Session) Set(key string, value string) (string, error) {
sessionsLock.Lock()
defer sessionsLock.Unlock()
switch key { switch key {
case "timezone": case "timezone":
s.Timezone = value s.Timezone = value
@ -177,13 +214,6 @@ func (s *Session) Set(key string, value string) (string, error) {
} }
s.AsciiArrows = b s.AsciiArrows = b
return value, nil return value, nil
case "muc":
b, err := toBool(value)
if err != nil {
return "", err
}
s.MUC = b
return value, nil
case "oobmode": case "oobmode":
b, err := toBool(value) b, err := toBool(value)
if err != nil { if err != nil {
@ -205,6 +235,20 @@ func (s *Session) Set(key string, value string) (string, error) {
} }
s.HideIds = b s.HideIds = b
return value, nil 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") return "", errors.New("Unknown session property")
@ -221,6 +265,51 @@ func (s *Session) TimezoneToLocation() *time.Location {
return zeroLocation 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 { func fromBool(b bool) string {
if b { if b {
return "true" return "true"

View file

@ -47,19 +47,20 @@ func TestSessionToMap(t *testing.T) {
session := Session{ session := Session{
Timezone: "klsf", Timezone: "klsf",
RawMessages: true, RawMessages: true,
MUC: true,
OOBMode: true, OOBMode: true,
Receipts: true,
} }
m := session.ToMap() m := session.ToMap()
sample := map[string]string{ sample := map[string]string{
"timezone": "klsf", "timezone": "klsf",
"keeponline": "false", "keeponline": "false",
"muc": "true",
"rawmessages": "true", "rawmessages": "true",
"asciiarrows": "false", "asciiarrows": "false",
"oobmode": "true", "oobmode": "true",
"carbons": "false", "carbons": "false",
"hideids": "false", "hideids": "false",
"receipts": "true",
"nativeedits": "false",
} }
if !reflect.DeepEqual(m, sample) { if !reflect.DeepEqual(m, sample) {
t.Errorf("Map does not match the sample: %v", m) t.Errorf("Map does not match the sample: %v", m)
@ -87,3 +88,31 @@ func TestSessionSetAbsent(t *testing.T) {
t.Error("There shouldn't come a donkey!") 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" "dev.narayana.im/narayana/telegabber/xmpp"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/zelenin/go-tdlib/client"
goxmpp "gosrc.io/xmpp" goxmpp "gosrc.io/xmpp"
) )
var version string = "2.0.0-dev" var version string = "1.9.6"
var commit string var commit string
var sm *goxmpp.StreamManager var sm *goxmpp.StreamManager
@ -60,6 +61,9 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
client.SetLogVerbosityLevel(&client.SetLogVerbosityLevelRequest{
NewVerbosityLevel: stringToTdlibLogConstant(config.Telegram.Loglevel),
})
SetLogrusLevel(config.XMPP.Loglevel) SetLogrusLevel(config.XMPP.Loglevel)
log.Infof("Starting telegabber version %v", version) 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() { func exit() {
xmpp.Close(component) xmpp.Close(component)
close(cleanupDone) 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,50 +16,12 @@ import (
"gosrc.io/xmpp" "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 // DelayedStatus describes an online status expiring on timeout
type DelayedStatus struct { type DelayedStatus struct {
TimestampOnline int64 TimestampOnline int64
TimestampExpired int64 TimestampExpired int64
} }
// MUCState holds MUC metadata
type MUCState struct {
Resources map[string]bool
Members map[int64]*MUCMember
}
// MUCMember represents a MUC member
type MUCMember struct {
Nickname string
Affiliation string
}
func NewMUCState() *MUCState {
return &MUCState{
Resources: make(map[string]bool),
Members: make(map[int64]*MUCMember),
}
}
// Client stores the metadata for lazily invoked TDlib instance // Client stores the metadata for lazily invoked TDlib instance
type Client struct { type Client struct {
client *client.Client client *client.Client
@ -72,19 +34,20 @@ type Client struct {
jid string jid string
Session *persistence.Session Session *persistence.Session
resources map[string]bool resources map[string]bool
outbox map[string]string
content *config.TelegramContentConfig content *config.TelegramContentConfig
cache *cache.Cache cache *cache.Cache
online bool online bool
outbox map[string]string
editOutbox map[string]string
DelayedStatuses map[int64]*DelayedStatus DelayedStatuses map[int64]*DelayedStatus
DelayedStatusesLock sync.Mutex DelayedStatusesLock sync.Mutex
lastMsgHashes map[int64]uint64 lastMsgHashes map[int64]uint64
lastMsgIds map[int64]string
msgHashSeed maphash.Seed msgHashSeed maphash.Seed
mucCache map[int64]*MUCState
locks clientLocks locks clientLocks
SendMessageLock sync.Mutex SendMessageLock sync.Mutex
} }
@ -94,8 +57,9 @@ type clientLocks struct {
chatMessageLocks map[int64]*sync.Mutex chatMessageLocks map[int64]*sync.Mutex
resourcesLock sync.Mutex resourcesLock sync.Mutex
outboxLock sync.Mutex outboxLock sync.Mutex
mucCacheLock sync.Mutex editOutboxLock sync.Mutex
lastMsgHashesLock sync.Mutex lastMsgHashesLock sync.Mutex
lastMsgIdsLock sync.RWMutex
authorizerReadLock sync.Mutex authorizerReadLock sync.Mutex
authorizerWriteLock sync.Mutex authorizerWriteLock sync.Mutex
@ -105,10 +69,6 @@ type clientLocks struct {
func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component, session *persistence.Session) (*Client, error) { func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component, session *persistence.Session) (*Client, error) {
var options []client.Option var options []client.Option
options = append(options, client.WithLogVerbosity(&client.SetLogVerbosityLevelRequest{
NewVerbosityLevel: stringToLogConstant(conf.Loglevel),
}))
if conf.Tdlib.Client.CatchTimeout != 0 { if conf.Tdlib.Client.CatchTimeout != 0 {
options = append(options, client.WithCatchTimeout( options = append(options, client.WithCatchTimeout(
time.Duration(conf.Tdlib.Client.CatchTimeout)*time.Second, time.Duration(conf.Tdlib.Client.CatchTimeout)*time.Second,
@ -154,13 +114,14 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component
jid: jid, jid: jid,
Session: session, Session: session,
resources: make(map[string]bool), resources: make(map[string]bool),
outbox: make(map[string]string),
mucCache: make(map[int64]*MUCState),
content: &conf.Content, content: &conf.Content,
cache: cache.NewCache(), cache: cache.NewCache(),
outbox: make(map[string]string),
editOutbox: make(map[string]string),
options: options, options: options,
DelayedStatuses: make(map[int64]*DelayedStatus), DelayedStatuses: make(map[int64]*DelayedStatus),
lastMsgHashes: make(map[int64]uint64), lastMsgHashes: make(map[int64]uint64),
lastMsgIds: make(map[int64]string),
msgHashSeed: maphash.MakeSeed(), msgHashSeed: maphash.MakeSeed(),
locks: clientLocks{ locks: clientLocks{
chatMessageLocks: make(map[int64]*sync.Mutex), 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"}, "invite": command{"id or @username", "add user to current chat"},
"link": command{"", "get invite link for current chat"}, "link": command{"", "get invite link for current chat"},
"kick": command{"id or @username", "remove user to current chat"}, "kick": command{"id or @username", "remove user to current chat"},
"mute": command{"id or @username [hours]", "mute user in current chat"}, "mute": command{"[id or @username] [hours]", "mute the whole chat or a user in current chat"},
"unmute": command{"id or @username", "unrestrict user from 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"}, "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)"}, "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"}, "promote": command{"id or @username [title]", "promote user to admin in current chat"},
@ -185,12 +185,27 @@ func keyValueString(key, value string) string {
} }
func (c *Client) unsubscribe(chatID int64) error { func (c *Client) unsubscribe(chatID int64) error {
return gateway.SendPresence( args := gateway.SimplePresence(chatID, "unsubscribed")
c.xmpp, return c.sendPresence(args...)
c.jid, }
gateway.SPFrom(strconv.FormatInt(chatID, 10)),
gateway.SPType("unsubscribed"), func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) {
) for i := len(messages) - 1; i >= 0; i-- {
message := messages[i]
reply, _ := c.getMessageReply(message, false, true)
gateway.SendMessage(
c.jid,
strconv.FormatInt(chatID, 10),
c.formatMessage(0, 0, false, message),
strconv.FormatInt(message.Id, 10),
c.xmpp,
reply,
"",
false,
false,
)
}
} }
func (c *Client) usernameOrIDToID(username string) (int64, error) { func (c *Client) usernameOrIDToID(username string) (int64, error) {
@ -264,16 +279,15 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
return notOnline return notOnline
} }
for _, id := range c.cache.ChatsKeys() {
c.unsubscribe(id)
}
_, err := c.client.LogOut() _, err := c.client.LogOut()
if err != nil { if err != nil {
c.forceClose()
return errors.Wrap(err, "Logout error").Error() return errors.Wrap(err, "Logout error").Error()
} }
for _, id := range c.cache.ChatsKeys() {
c.unsubscribe(id)
}
c.Session.Login = "" c.Session.Login = ""
// cancel auth // cancel auth
case "cancelauth": case "cancelauth":
@ -366,6 +380,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
} }
case "config": case "config":
if len(args) > 1 { if len(args) > 1 {
var msg string
if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" {
return "The server did not allow to enable carbons" return "The server did not allow to enable carbons"
} }
@ -376,7 +391,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
} }
gateway.DirtySessions = true 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 { } else if len(args) > 0 {
value, err := c.Session.Get(args[0]) value, err := c.Session.Get(args[0])
if err != nil { if err != nil {
@ -405,7 +420,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
text := rawCmdArguments(cmdline, 1) text := rawCmdArguments(cmdline, 1)
_, err = c.client.ReportChat(&client.ReportChatRequest{ _, err = c.client.ReportChat(&client.ReportChatRequest{
ChatId: contact.Id, ChatId: contact.Id,
Reason: &client.ChatReportReasonCustom{}, Reason: &client.ReportReasonCustom{},
Text: text, Text: text,
}) })
if err != nil { if err != nil {
@ -693,18 +708,18 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
} }
// blacklists current user // blacklists current user
case "block": case "block":
_, err := c.client.ToggleMessageSenderIsBlocked(&client.ToggleMessageSenderIsBlockedRequest{ _, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{
SenderId: &client.MessageSenderUser{UserId: chatID}, SenderId: &client.MessageSenderUser{UserId: chatID},
IsBlocked: true, BlockList: &client.BlockListMain{},
}) })
if err != nil { if err != nil {
return err.Error(), true return err.Error(), true
} }
// unblacklists current user // unblacklists current user
case "unblock": case "unblock":
_, err := c.client.ToggleMessageSenderIsBlocked(&client.ToggleMessageSenderIsBlockedRequest{ _, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{
SenderId: &client.MessageSenderUser{UserId: chatID}, SenderId: &client.MessageSenderUser{UserId: chatID},
IsBlocked: false, BlockList: nil,
}) })
if err != nil { if err != nil {
return err.Error(), true return err.Error(), true
@ -756,59 +771,65 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
if err != nil { if err != nil {
return err.Error(), true return err.Error(), true
} }
// mute @username [n hours] // mute [@username [n hours]]
case "mute": case "mute":
if len(args) < 1 { if len(args) > 0 {
return notEnoughArguments, true contact, _, err := c.GetContactByUsername(args[0])
}
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 err != nil { if err != nil {
return "Invalid number of hours", true return err.Error(), true
} }
}
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ var hours int64
ChatId: chatID, if len(args) > 1 {
MemberId: &client.MessageSenderUser{UserId: contact.Id}, hours, err = strconv.ParseInt(args[1], 10, 32)
Status: &client.ChatMemberStatusRestricted{ if err != nil {
IsMember: true, return "Invalid number of hours", true
RestrictedUntilDate: c.formatBantime(hours), }
Permissions: &permissionsReadonly, }
},
}) _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
if err != nil { ChatId: chatID,
return err.Error(), true 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": case "unmute":
if len(args) < 1 { if len(args) > 0 {
return notEnoughArguments, true contact, _, err := c.GetContactByUsername(args[0])
} if err != nil {
return err.Error(), true
}
contact, _, err := c.GetContactByUsername(args[0]) _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{
if err != nil { ChatId: chatID,
return err.Error(), true MemberId: &client.MessageSenderUser{UserId: contact.Id},
} Status: &client.ChatMemberStatusRestricted{
IsMember: true,
_, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ RestrictedUntilDate: 0,
ChatId: chatID, Permissions: &permissionsMember,
MemberId: &client.MessageSenderUser{UserId: contact.Id}, },
Status: &client.ChatMemberStatusRestricted{ })
IsMember: true, if err != nil {
RestrictedUntilDate: 0, return err.Error(), true
Permissions: &permissionsMember, }
}, } else {
}) if !c.Session.UnignoreChat(chatID) {
if err != nil { return "Chat wasn't ignored", true
return err.Error(), true }
gateway.DirtySessions = true
} }
// ban @username from current chat [for N hours] // ban @username from current chat [for N hours]
case "ban": case "ban":
@ -988,7 +1009,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
return err.Error(), true return err.Error(), true
} }
c.sendMessagesReverse(chatID, messages.Messages, true, "") c.sendMessagesReverse(chatID, messages.Messages)
// get latest entries from history // get latest entries from history
case "history": case "history":
var limit int32 = 10 var limit int32 = 10
@ -999,11 +1020,32 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
} }
} }
messages, err := c.getNLastMessages(chatID, limit) var newMessages *client.Messages
if err != nil { var messages []*client.Message
return err.Error(), true var err error
var fromId int64
for _ = range make([]struct{}, limit) { // safety limit
if len(messages) > 0 {
fromId = messages[len(messages)-1].Id
}
newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{
ChatId: chatID,
FromMessageId: fromId,
Limit: limit,
})
if err != nil {
return err.Error(), true
}
messages = append(messages, newMessages.Messages...)
if len(newMessages.Messages) == 0 || len(messages) >= int(limit) {
break
}
} }
c.sendMessagesReverse(chatID, messages, true, "")
c.sendMessagesReverse(chatID, messages)
// chat members // chat members
case "members": case "members":
var query string var query string

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import (
) )
func TestNoFormatting(t *testing.T) { func TestNoFormatting(t *testing.T) {
markup := Format("abc\ndef", []*client.TextEntity{}, EntityToMarkdown) markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeMarkdown)
if markup != "abc\ndef" { if markup != "abc\ndef" {
t.Errorf("No formatting expected, but: %v", markup) t.Errorf("No formatting expected, but: %v", markup)
} }
@ -20,7 +20,7 @@ func TestFormattingSimple(t *testing.T) {
Length: 4, Length: 4,
Type: &client.TextEntityTypeBold{}, Type: &client.TextEntityTypeBold{},
}, },
}, EntityToMarkdown) }, MarkupModeMarkdown)
if markup != "👙**🐧🐖**" { if markup != "👙**🐧🐖**" {
t.Errorf("Wrong simple formatting: %v", markup) t.Errorf("Wrong simple formatting: %v", markup)
} }
@ -40,7 +40,7 @@ func TestFormattingAdjacent(t *testing.T) {
Url: "https://narayana.im/", Url: "https://narayana.im/",
}, },
}, },
}, EntityToMarkdown) }, MarkupModeMarkdown)
if markup != "a👙_🐧_[🐖](https://narayana.im/)" { if markup != "a👙_🐧_[🐖](https://narayana.im/)" {
t.Errorf("Wrong adjacent formatting: %v", markup) t.Errorf("Wrong adjacent formatting: %v", markup)
} }
@ -63,18 +63,18 @@ func TestFormattingAdjacentAndNested(t *testing.T) {
Length: 2, Length: 2,
Type: &client.TextEntityTypeItalic{}, Type: &client.TextEntityTypeItalic{},
}, },
}, EntityToMarkdown) }, MarkupModeMarkdown)
if markup != "```\n**👙**🐧\n```_🐖_" { if markup != "```\n**👙**🐧\n```_🐖_" {
t.Errorf("Wrong adjacent&nested formatting: %v", markup) t.Errorf("Wrong adjacent&nested formatting: %v", markup)
} }
} }
func TestRebalanceTwoZero(t *testing.T) { func TestRebalanceTwoZero(t *testing.T) {
s1 := InsertionStack{ s1 := insertionStack{
&Insertion{Offset: 7}, &insertion{Offset: 7},
&Insertion{Offset: 8}, &insertion{Offset: 8},
} }
s2 := InsertionStack{} s2 := insertionStack{}
s1, s2 = s1.rebalance(s2, 7) s1, s2 = s1.rebalance(s2, 7)
if !(len(s1) == 2 && len(s2) == 0 && s1[0].Offset == 7 && s1[1].Offset == 8) { if !(len(s1) == 2 && len(s2) == 0 && s1[0].Offset == 7 && s1[1].Offset == 8) {
t.Errorf("Wrong rebalance 20: %#v %#v", s1, s2) t.Errorf("Wrong rebalance 20: %#v %#v", s1, s2)
@ -82,13 +82,13 @@ func TestRebalanceTwoZero(t *testing.T) {
} }
func TestRebalanceNeeded(t *testing.T) { func TestRebalanceNeeded(t *testing.T) {
s1 := InsertionStack{ s1 := insertionStack{
&Insertion{Offset: 7}, &insertion{Offset: 7},
&Insertion{Offset: 8}, &insertion{Offset: 8},
} }
s2 := InsertionStack{ s2 := insertionStack{
&Insertion{Offset: 10}, &insertion{Offset: 10},
&Insertion{Offset: 9}, &insertion{Offset: 9},
} }
s1, s2 = s1.rebalance(s2, 9) s1, s2 = s1.rebalance(s2, 9)
if !(len(s1) == 3 && len(s2) == 1 && if !(len(s1) == 3 && len(s2) == 1 &&
@ -99,13 +99,13 @@ func TestRebalanceNeeded(t *testing.T) {
} }
func TestRebalanceNotNeeded(t *testing.T) { func TestRebalanceNotNeeded(t *testing.T) {
s1 := InsertionStack{ s1 := insertionStack{
&Insertion{Offset: 7}, &insertion{Offset: 7},
&Insertion{Offset: 8}, &insertion{Offset: 8},
} }
s2 := InsertionStack{ s2 := insertionStack{
&Insertion{Offset: 10}, &insertion{Offset: 10},
&Insertion{Offset: 9}, &insertion{Offset: 9},
} }
s1, s2 = s1.rebalance(s2, 8) s1, s2 = s1.rebalance(s2, 8)
if !(len(s1) == 2 && len(s2) == 2 && if !(len(s1) == 2 && len(s2) == 2 &&
@ -116,13 +116,13 @@ func TestRebalanceNotNeeded(t *testing.T) {
} }
func TestRebalanceLate(t *testing.T) { func TestRebalanceLate(t *testing.T) {
s1 := InsertionStack{ s1 := insertionStack{
&Insertion{Offset: 7}, &insertion{Offset: 7},
&Insertion{Offset: 8}, &insertion{Offset: 8},
} }
s2 := InsertionStack{ s2 := insertionStack{
&Insertion{Offset: 10}, &insertion{Offset: 10},
&Insertion{Offset: 9}, &insertion{Offset: 9},
} }
s1, s2 = s1.rebalance(s2, 10) s1, s2 = s1.rebalance(s2, 10)
if !(len(s1) == 4 && len(s2) == 0 && if !(len(s1) == 4 && len(s2) == 0 &&
@ -133,7 +133,7 @@ func TestRebalanceLate(t *testing.T) {
} }
func TestIteratorEmpty(t *testing.T) { func TestIteratorEmpty(t *testing.T) {
s := InsertionStack{} s := insertionStack{}
g := s.NewIterator() g := s.NewIterator()
v := g() v := g()
if v != nil { if v != nil {
@ -142,9 +142,9 @@ func TestIteratorEmpty(t *testing.T) {
} }
func TestIterator(t *testing.T) { func TestIterator(t *testing.T) {
s := InsertionStack{ s := insertionStack{
&Insertion{Offset: 7}, &insertion{Offset: 7},
&Insertion{Offset: 8}, &insertion{Offset: 8},
} }
g := s.NewIterator() g := s.NewIterator()
v := g() v := g()
@ -208,7 +208,7 @@ func TestSortEmpty(t *testing.T) {
} }
func TestNoFormattingXEP0393(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" { if markup != "abc\ndef" {
t.Errorf("No formatting expected, but: %v", markup) t.Errorf("No formatting expected, but: %v", markup)
} }
@ -221,7 +221,7 @@ func TestFormattingXEP0393Simple(t *testing.T) {
Length: 4, Length: 4,
Type: &client.TextEntityTypeBold{}, Type: &client.TextEntityTypeBold{},
}, },
}, EntityToXEP0393) }, MarkupModeXEP0393)
if markup != "👙*🐧🐖*" { if markup != "👙*🐧🐖*" {
t.Errorf("Wrong simple formatting: %v", markup) t.Errorf("Wrong simple formatting: %v", markup)
} }
@ -241,7 +241,7 @@ func TestFormattingXEP0393Adjacent(t *testing.T) {
Url: "https://narayana.im/", Url: "https://narayana.im/",
}, },
}, },
}, EntityToXEP0393) }, MarkupModeXEP0393)
if markup != "a👙_🐧_🐖 <https://narayana.im/>" { if markup != "a👙_🐧_🐖 <https://narayana.im/>" {
t.Errorf("Wrong adjacent formatting: %v", markup) t.Errorf("Wrong adjacent formatting: %v", markup)
} }
@ -264,7 +264,7 @@ func TestFormattingXEP0393AdjacentAndNested(t *testing.T) {
Length: 2, Length: 2,
Type: &client.TextEntityTypeItalic{}, Type: &client.TextEntityTypeItalic{},
}, },
}, EntityToXEP0393) }, MarkupModeXEP0393)
if markup != "```\n*👙*🐧\n```_🐖_" { if markup != "```\n*👙*🐧\n```_🐖_" {
t.Errorf("Wrong adjacent&nested formatting: %v", markup) t.Errorf("Wrong adjacent&nested formatting: %v", markup)
} }
@ -287,7 +287,7 @@ func TestFormattingXEP0393AdjacentItalicBoldItalic(t *testing.T) {
Length: 69, Length: 69,
Type: &client.TextEntityTypeItalic{}, Type: &client.TextEntityTypeItalic{},
}, },
}, EntityToXEP0393) }, MarkupModeXEP0393)
if markup != "_раса двуногих крысолюдей, *которую так редко замечают, что многие отрицают само их существование*_" { if markup != "_раса двуногих крысолюдей, *которую так редко замечают, что многие отрицают само их существование*_" {
t.Errorf("Wrong adjacent italic/bold-italic formatting: %v", markup) t.Errorf("Wrong adjacent italic/bold-italic formatting: %v", markup)
} }
@ -315,7 +315,7 @@ func TestFormattingXEP0393MultipleAdjacent(t *testing.T) {
Length: 1, Length: 1,
Type: &client.TextEntityTypeItalic{}, Type: &client.TextEntityTypeItalic{},
}, },
}, EntityToXEP0393) }, MarkupModeXEP0393)
if markup != "a*bcd*_e_" { if markup != "a*bcd*_e_" {
t.Errorf("Wrong multiple adjacent formatting: %v", markup) t.Errorf("Wrong multiple adjacent formatting: %v", markup)
} }
@ -343,7 +343,7 @@ func TestFormattingXEP0393Intersecting(t *testing.T) {
Length: 1, Length: 1,
Type: &client.TextEntityTypeBold{}, Type: &client.TextEntityTypeBold{},
}, },
}, EntityToXEP0393) }, MarkupModeXEP0393)
if markup != "a*b*_*cd*e_" { if markup != "a*b*_*cd*e_" {
t.Errorf("Wrong intersecting formatting: %v", markup) t.Errorf("Wrong intersecting formatting: %v", markup)
} }
@ -361,7 +361,7 @@ func TestFormattingXEP0393InlineCode(t *testing.T) {
Length: 25, Length: 25,
Type: &client.TextEntityTypePre{}, Type: &client.TextEntityTypePre{},
}, },
}, EntityToXEP0393) }, MarkupModeXEP0393)
if markup != "Is `Gajim` a thing?\n\n```\necho 'Hello'\necho 'world'\n```\n\nhruck(" { if markup != "Is `Gajim` a thing?\n\n```\necho 'Hello'\necho 'world'\n```\n\nhruck(" {
t.Errorf("Wrong intersecting formatting: %v", markup) t.Errorf("Wrong intersecting formatting: %v", markup)
} }
@ -374,7 +374,7 @@ func TestFormattingMarkdownStrikethrough(t *testing.T) {
Length: 3, Length: 3,
Type: &client.TextEntityTypeStrikethrough{}, Type: &client.TextEntityTypeStrikethrough{},
}, },
}, EntityToMarkdown) }, MarkupModeMarkdown)
if markup != "Everyone ~~dis~~likes cake." { if markup != "Everyone ~~dis~~likes cake." {
t.Errorf("Wrong strikethrough formatting: %v", markup) t.Errorf("Wrong strikethrough formatting: %v", markup)
} }
@ -387,14 +387,14 @@ func TestFormattingXEP0393Strikethrough(t *testing.T) {
Length: 3, Length: 3,
Type: &client.TextEntityTypeStrikethrough{}, Type: &client.TextEntityTypeStrikethrough{},
}, },
}, EntityToXEP0393) }, MarkupModeXEP0393)
if markup != "Everyone ~dis~likes cake." { if markup != "Everyone ~dis~likes cake." {
t.Errorf("Wrong strikethrough formatting: %v", markup) t.Errorf("Wrong strikethrough formatting: %v", markup)
} }
} }
func TestClaspLeft(t *testing.T) { func TestClaspLeft(t *testing.T) {
text := "a b c" text := textToDoubledRunes("a b c")
entities := []*client.TextEntity{ entities := []*client.TextEntity{
&client.TextEntity{ &client.TextEntity{
Offset: 1, Offset: 1,
@ -409,7 +409,7 @@ func TestClaspLeft(t *testing.T) {
} }
func TestClaspBoth(t *testing.T) { func TestClaspBoth(t *testing.T) {
text := "a b c" text := textToDoubledRunes("a b c")
entities := []*client.TextEntity{ entities := []*client.TextEntity{
&client.TextEntity{ &client.TextEntity{
Offset: 1, Offset: 1,
@ -424,7 +424,7 @@ func TestClaspBoth(t *testing.T) {
} }
func TestClaspNotNeeded(t *testing.T) { func TestClaspNotNeeded(t *testing.T) {
text := " abc " text := textToDoubledRunes(" abc ")
entities := []*client.TextEntity{ entities := []*client.TextEntity{
&client.TextEntity{ &client.TextEntity{
Offset: 1, Offset: 1,
@ -439,7 +439,7 @@ func TestClaspNotNeeded(t *testing.T) {
} }
func TestClaspNested(t *testing.T) { func TestClaspNested(t *testing.T) {
text := "a b c" text := textToDoubledRunes("a b c")
entities := []*client.TextEntity{ entities := []*client.TextEntity{
&client.TextEntity{ &client.TextEntity{
Offset: 1, Offset: 1,
@ -459,7 +459,7 @@ func TestClaspNested(t *testing.T) {
} }
func TestClaspEmoji(t *testing.T) { func TestClaspEmoji(t *testing.T) {
text := "a 🐖 c" text := textToDoubledRunes("a 🐖 c")
entities := []*client.TextEntity{ entities := []*client.TextEntity{
&client.TextEntity{ &client.TextEntity{
Offset: 1, Offset: 1,
@ -472,3 +472,111 @@ func TestClaspEmoji(t *testing.T) {
t.Errorf("Wrong claspemoji: %#v", entities) 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() { func (c *Client) updateHandler() {
listener := c.client.GetListener() listener := c.client.GetListener()
defer listener.Close() defer listener.Close()
@ -141,6 +166,12 @@ func (c *Client) updateHandler() {
uhOh() uhOh()
} }
c.updateChatTitle(typedUpdate) c.updateChatTitle(typedUpdate)
case client.TypeUpdateChatReadOutbox:
typedUpdate, ok := update.(*client.UpdateChatReadOutbox)
if !ok {
uhOh()
}
c.updateChatReadOutbox(typedUpdate)
default: default:
// log only handled types // log only handled types
continue continue
@ -153,13 +184,6 @@ func (c *Client) updateHandler() {
// new user discovered // new user discovered
func (c *Client) updateUser(update *client.UpdateUser) { func (c *Client) updateUser(update *client.UpdateUser) {
// check if MUC nicknames should be updated
cacheUser, ok := c.cache.GetUser(update.User.Id)
if ok && (cacheUser.FirstName != update.User.FirstName || cacheUser.LastName != update.User.LastName) {
newNickname := c.GetMUCNickname(update.User.Id)
c.updateMUCsNickname(update.User.Id, newNickname)
}
c.cache.SetUser(update.User.Id, update.User) c.cache.SetUser(update.User.Id, update.User)
show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id) show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id)
go c.ProcessStatusUpdate(update.User.Id, status, show, gateway.SPType(presenceType)) go c.ProcessStatusUpdate(update.User.Id, status, show, gateway.SPType(presenceType))
@ -211,6 +235,9 @@ func (c *Client) updateChatLastMessage(update *client.UpdateChatLastMessage) {
// message received // message received
func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { func (c *Client) updateNewMessage(update *client.UpdateNewMessage) {
chatId := update.Message.ChatId chatId := update.Message.ChatId
if c.Session.IsChatIgnored(chatId) {
return
}
// guarantee sequential message delivering per chat // guarantee sequential message delivering per chat
lock := c.getChatMessageLock(chatId) lock := c.getChatMessageLock(chatId)
@ -218,6 +245,8 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content)
// ignore self outgoing messages // ignore self outgoing messages
if update.Message.IsOutgoing && if update.Message.IsOutgoing &&
update.Message.SendingState != nil && update.Message.SendingState != nil &&
@ -230,23 +259,31 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) {
}).Warn("New message from chat") }).Warn("New message from chat")
c.ProcessIncomingMessage(chatId, update.Message) c.ProcessIncomingMessage(chatId, update.Message)
c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content)
}() }()
} }
// message content updated // message content updated
func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
if c.Session.IsChatIgnored(update.ChatId) {
return
}
markupFunction := c.getFormatter() markupFunction := c.getFormatter()
defer c.updateLastMessageHash(update.ChatId, update.MessageId, update.NewContent) 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.Lock()
c.SendMessageLock.Unlock() 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 var ignoredResource string
if err == nil { if xmppIdErr == nil {
ignoredResource = c.popFromOutbox(xmppId) ignoredResource = c.popFromEditOutbox(xmppId)
} else { } else {
log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.MessageId) log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.MessageId)
} }
@ -260,19 +297,62 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
if update.NewContent.MessageContentType() == client.TypeMessageText && c.hasLastMessageHashChanged(update.ChatId, update.MessageId, update.NewContent) { if update.NewContent.MessageContentType() == client.TypeMessageText && c.hasLastMessageHashChanged(update.ChatId, update.MessageId, update.NewContent) {
textContent := update.NewContent.(*client.MessageText) textContent := update.NewContent.(*client.MessageText)
var editChar string log.Debugf("textContent: %#v", textContent.Text)
if c.Session.AsciiArrows {
editChar = "e " var replaceId string
} else { sId := strconv.FormatInt(update.MessageId, 10)
editChar = "✎ " 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.Text,
textContent.Text.Entities, textContent.Text.Entities,
markupFunction, markupFunction,
)) ))
sChatId := strconv.FormatInt(update.ChatId, 10)
for _, jid := range jids { for _, jid := range jids {
gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, 0, false, false, "") gateway.SendMessage(jid, sChatId, text.String(), "e"+sId, c.xmpp, nil, replaceId, isCarbon, false)
} }
} }
} }
@ -280,6 +360,10 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
// message(s) deleted // message(s) deleted
func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) {
if update.IsPermanent { if update.IsPermanent {
if c.Session.IsChatIgnored(update.ChatId) {
return
}
var deleteChar string var deleteChar string
if c.Session.AsciiArrows { if c.Session.AsciiArrows {
deleteChar = "X " deleteChar = "X "
@ -301,19 +385,25 @@ func (c *Client) updateAuthorizationState(update *client.UpdateAuthorizationStat
} }
} }
// clean uploaded files
func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) { 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) 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 { 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()) 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) file, _ := c.contentToFile(update.Message.Content)
if file != nil && file.Local != nil { if file != nil && file.Local != nil {
c.cleanTempFile(file.Local.Path) c.cleanTempFile(file.Local.Path)
} }
} }
func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed) { func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed) {
// clean uploaded files
file, _ := c.contentToFile(update.Message.Content) file, _ := c.contentToFile(update.Message.Content)
if file != nil && file.Local != nil { if file != nil && file.Local != nil {
c.cleanTempFile(file.Local.Path) c.cleanTempFile(file.Local.Path)
@ -322,14 +412,10 @@ func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed)
// chat title changed // chat title changed
func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { func (c *Client) updateChatTitle(update *client.UpdateChatTitle) {
chat, user, _ := c.GetContactByID(update.ChatId, nil)
if c.Session.MUC && c.IsGroup(chat) {
return
}
gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp) gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp)
// set also the status (for group chats only) // set also the status (for group chats only)
chat, user, _ := c.GetContactByID(update.ChatId, nil)
if user == nil { if user == nil {
c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true)) c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true))
} }
@ -339,3 +425,7 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) {
chat.Title = update.Title chat.Title = update.Title
} }
} }
func (c *Client) updateChatReadOutbox(update *client.UpdateChatReadOutbox) {
c.sendMarker(update.ChatId, update.LastReadOutboxMessageId, gateway.MarkerTypeDisplayed)
}

File diff suppressed because it is too large Load diff

View file

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

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

View file

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

View file

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