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