diff --git a/README.md b/README.md
index 18299a54e..d08f2a2d3 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
Tinode is *not* XMPP/Jabber. It is *not* compatible with XMPP. It's meant as a replacement for XMPP. On the surface, it's a lot like open source WhatsApp or Telegram.
-Version 0.20. This is beta-quality software: feature-complete and stable but probably with a few bugs or missing features. Follow [instructions](INSTALL.md) to install and run or use one of the cloud services below. Read [API documentation](docs/API.md).
+This is beta-quality software: feature-complete and stable but probably with a few bugs or missing features. Follow [instructions](INSTALL.md) to install and run or use one of the cloud services below. Read [API documentation](docs/API.md).
diff --git a/docs/API.md b/docs/API.md
index dc3a11f94..df93bff05 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -1280,7 +1280,7 @@ Tinode uses `{pres}` message to inform clients of important events. A separate [
pres: {
topic: "me", // string, topic which receives the notification, always present
src: "grp1XUtEhjv6HND", // string, topic or user affected by the change, always present
- what: "on", // string, what's changed, always present
+ what: "on", // string, action type, what's changed, always present
seq: 123, // integer, "what" is "msg", a server-issued ID of the message,
// optional
clear: 15, // integer, "what" is "del", an update to the delete transaction ID.
@@ -1295,6 +1295,22 @@ pres: {
}
```
+The following action types are currently defined:
+
+ * on: topic or user came online
+ * off: topic or user went offline
+ * ua: user agent changed, for example user was logged in with one client, then logged in with another
+ * upd: topic description has changed
+ * tags: topic tags have changed
+ * acs: access permissions have changed
+ * gone: topic is no longer available, for example, it was deleted or you were unsubscribed from it
+ * term: subscription to topic has been terminated, you may try to resubscribe
+ * msg: a new message is available
+ * read: one or more messages have been read by the recipient
+ * recv: one or more messages have been received by the recipient
+ * del: messages were deleted
+
+
The `{pres}` messages are purely transient: they are not stored and no attempt is made to deliver them later if the destination is temporarily unavailable.
Timestamp is not present in `{pres}` messages.
diff --git a/server/hdl_files.go b/server/hdl_files.go
index 89f4012cf..8a6e74aae 100644
--- a/server/hdl_files.go
+++ b/server/hdl_files.go
@@ -262,7 +262,7 @@ func largeFileReceive(wrt http.ResponseWriter, req *http.Request) {
}
writeHttpResponse(NoErrParams(msgID, "", now, params), nil)
- logs.Info.Println("media upload: ok", fdef.Location)
+ logs.Info.Println("media upload: ok", fdef.Id, fdef.Location)
}
// largeFileRunGarbageCollection runs every 'period' and deletes up to 'blockSize' unused files.
diff --git a/server/media/media.go b/server/media/media.go
index e6d8260b9..a3f69a965 100644
--- a/server/media/media.go
+++ b/server/media/media.go
@@ -5,6 +5,7 @@ import (
"io"
"net/http"
"path"
+ "regexp"
"strings"
"github.com/tinode/chat/server/store/types"
@@ -40,6 +41,8 @@ type Handler interface {
GetIdFromUrl(url string) types.Uid
}
+var fileNamePattern = regexp.MustCompile(`^[-_A-Za-z0-9]+`)
+
// GetIdFromUrl is a helper method for extracting file ID from a URL.
func GetIdFromUrl(url, serveUrl string) types.Uid {
dir, fname := path.Split(path.Clean(url))
@@ -48,7 +51,7 @@ func GetIdFromUrl(url, serveUrl string) types.Uid {
return types.ZeroUid
}
- return types.ParseUid(strings.Split(fname, ".")[0])
+ return types.ParseUid(fileNamePattern.FindString(fname))
}
// matchCORSOrigin compares origin from the HTTP request to a list of allowed origins.
diff --git a/server/store/types/types.go b/server/store/types/types.go
index 1bea0e9cb..e744dfa86 100644
--- a/server/store/types/types.go
+++ b/server/store/types/types.go
@@ -922,6 +922,9 @@ type Subscription struct {
// Topic's or user's state.
state ObjState
+
+ // This is not a fully initialized subscription object
+ dummy bool
}
// SetPublic assigns a value to `public`, otherwise not accessible from outside the package.
@@ -1032,6 +1035,16 @@ func (s *Subscription) SetState(state ObjState) {
s.state = state
}
+// SetDummy marks this subscription object as only partially intialized.
+func (s *Subscription) SetDummy(dummy bool) {
+ s.dummy = dummy
+}
+
+// IsDummy is true if this subscription object as only partially intialized.
+func (s *Subscription) IsDummy() bool {
+ return s.dummy
+}
+
// Contact is a result of a search for connections
type Contact struct {
Id string
diff --git a/server/topic.go b/server/topic.go
index 8020e0986..48355e398 100644
--- a/server/topic.go
+++ b/server/topic.go
@@ -2368,6 +2368,30 @@ func (t *Topic) replyGetSub(sess *Session, asUid types.Uid, authLevel auth.Level
} else {
// User manages cache. Include deleted subscriptions too.
subs, err = store.Users.GetTopicsAny(asUid, msgOpts2storeOpts(req))
+
+ // Returned subscriptions do not contain topics which are online now but otherwise unchanged.
+ // We need to add these topic to the list otherwise the user would see them as offline.
+ selected := map[string]struct{}{}
+ for i := range subs {
+ sub := &subs[i]
+ with := sub.GetWith()
+ if with != "" {
+ selected[with] = struct{}{}
+ } else {
+ selected[sub.Topic] = struct{}{}
+ }
+ }
+
+ // Add dummy subscriptions for missing online topics.
+ for topic, psd := range t.perSubs {
+ _, present := selected[topic]
+ if !present && psd.online {
+ sub := types.Subscription{Topic: topic}
+ sub.SetWith(topic)
+ sub.SetDummy(true)
+ subs = append(subs, sub)
+ }
+ }
}
case types.TopicCatFnd:
// Select public or private query. Public has priority.
@@ -2500,15 +2524,15 @@ func (t *Topic) replyGetSub(sess *Session, asUid types.Uid, authLevel auth.Level
if !deleted && !banned {
if isReader {
- if sub.GetTouchedAt().IsZero() {
+ touchedAt := sub.GetTouchedAt()
+ if touchedAt.IsZero() {
mts.TouchedAt = nil
} else {
- touchedAt := sub.GetTouchedAt()
mts.TouchedAt = &touchedAt
}
mts.SeqId = sub.GetSeqId()
mts.DelId = sub.DelId
- } else {
+ } else if !sub.UpdatedAt.IsZero() {
mts.TouchedAt = &sub.UpdatedAt
}
@@ -2546,7 +2570,9 @@ func (t *Topic) replyGetSub(sess *Session, asUid types.Uid, authLevel auth.Level
}
if !deleted {
- mts.UpdatedAt = &sub.UpdatedAt
+ if !sub.UpdatedAt.IsZero() {
+ mts.UpdatedAt = &sub.UpdatedAt
+ }
if isReader && !banned {
mts.ReadSeqId = sub.ReadSeqId
mts.RecvSeqId = sub.RecvSeqId
@@ -2554,7 +2580,7 @@ func (t *Topic) replyGetSub(sess *Session, asUid types.Uid, authLevel auth.Level
if t.cat != types.TopicCatFnd {
// p2p and grp
- if sharer || uid == asUid || subMode.IsAdmin() {
+ if !sub.IsDummy() && (sharer || uid == asUid || subMode.IsAdmin()) {
// If user is not a sharer, the access mode of other ordinary users if not accessible.
// Own and admin permissions only are visible to non-sharers.
mts.Acs.Mode = subMode.String()
diff --git a/server/utils_test.go b/server/utils_test.go
index 5adb48548..345dbb9a4 100644
--- a/server/utils_test.go
+++ b/server/utils_test.go
@@ -28,23 +28,23 @@ func TestStringSliceDelta(t *testing.T) {
// - expected outputs: added, removed, intersection
cases := [][5][]string{
{
- []string{"abc", "def", "fff"}, []string{},
- []string{}, []string{"abc", "def", "fff"}, []string{},
+ {"abc", "def", "fff"}, {},
+ {}, {"abc", "def", "fff"}, {},
},
{
{}, {}, {}, {}, {},
},
{
{"aa", "xx", "bb", "aa", "bb"}, {"yy", "aa"},
- {"yy"}, []string{"aa", "bb", "bb", "xx"}, []string{"aa"},
+ {"yy"}, {"aa", "bb", "bb", "xx"}, {"aa"},
},
{
- []string{"bb", "aa", "bb"}, []string{"yy", "aa", "bb", "zzz", "zzz", "cc"},
- []string{"cc", "yy", "zzz", "zzz"}, []string{"bb"}, []string{"aa", "bb"},
+ {"bb", "aa", "bb"}, {"yy", "aa", "bb", "zzz", "zzz", "cc"},
+ {"cc", "yy", "zzz", "zzz"}, {"bb"}, {"aa", "bb"},
},
{
- []string{"aa", "aa", "aa"}, []string{"aa", "aa", "aa"},
- []string{}, []string{}, []string{"aa", "aa", "aa"},
+ {"aa", "aa", "aa"}, {"aa", "aa", "aa"},
+ {}, {}, {"aa", "aa", "aa"},
},
}
diff --git a/tn-cli/tn-cli.py b/tn-cli/tn-cli.py
index 980059615..27404dfb3 100644
--- a/tn-cli/tn-cli.py
+++ b/tn-cli/tn-cli.py
@@ -202,7 +202,7 @@ def encode_to_bytes(src):
if src == None:
return None
if isinstance(src, str):
- return src.encode('utf-8')
+ return ('"' + src + '"').encode()
return json.dumps(src).encode('utf-8')
# Parse credentials