Skip to content

Commit

Permalink
refactoring public/private for fnd topic
Browse files Browse the repository at this point in the history
  • Loading branch information
or-else committed May 18, 2018
1 parent f13a1e6 commit ca306d7
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 81 deletions.
26 changes: 13 additions & 13 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ Message `{get what="sub"}` to `me` is different from any other topic as it retur

Message `{get what="data"}` to `me` queries the history of invites/notifications. It's handled the same way as to any other topic.

### `fnd` and Tags: Contacts and Topics Discovery
### `fnd` and Tags: Finding Users and Topics

Topic `fnd` is automatically created for every user at the account creation time. It serves as an endpoint for discovering other users and group topics.

Expand All @@ -748,7 +748,7 @@ A tag is an arbitrary case-insensitive Unicode string (forced to lowercase) star

The `tags` are indexed server-side and used in user and topic discovery. Seach returns users and topics sorted by the number of matched tags in descending order.

In order to find users or topics, a user sets either `public` or `private` parameter of the `fnd` topic to a search query (see [Query language](#query_language) below) then issues a `{get topic="fnd" what="sub"}` request. If both `public` and `private` are set, `private` query is used. The `public` query is persisted accress sessions and devices, i.e. all user's sessions see the same `public` query. The value of `private` query is ephemeral, i.e. not saved to database and not shared between user's sessions. The `public` query is intended for large queries which do not change often, such as finding matches for everyone in user's contact list on mobile phone. The `private` query is intended to be short and specific, such as finding some topic or a user who is not in the contact list.
In order to find users or topics, a user sets either `public` or `private` parameter of the `fnd` topic to a search query (see [Query language](#query_language) below) then issues a `{get topic="fnd" what="sub"}` request. If both `public` and `private` are set, the `public` query is used. The `private` query is persisted accress sessions and devices, i.e. all user's sessions see the same `private` query. The value of `public` query is ephemeral, i.e. it's not saved to database and not shared between user's sessions. The `private` query is intended for large queries which do not change often, such as finding matches for everyone in user's contact list on mobile phone. The `public` query is intended to be short and specific, such as finding some topic or a user who is not in the contact list.

The system responds with a `{meta}` message with the `sub` section listing details of the found users or topics formatted as subscriptions.

Expand All @@ -758,17 +758,6 @@ Topic `fnd` is read-only. `{pub}` messages to `fnd` are rejected.

[Plugins](./pbx) support `Find` service which can be used to replace default search with a custom one.


#### Some use cases
* Restricting users to organizations.
An immutable tag(s) may be assigned to the user which denotes the organization the user belons to. When the user searches for other users or topics, the search can be restricted to always contain the tag. This approach can be used to segment users into organizations with limited visiblity into each other.

* Search by geographical location.
Client software may periodically assign a [geohash](https://en.wikipedia.org/wiki/Geohash) tag to the user based on current location. Searching for users in a given area would mean matching on geohash tags.

* Search by numerical range, such as age range.
The approach is similar to geohashing. The entire range of numbers is covered by the smallest possible power of 2, for instance the range of human ages is covered by 2<sup>7</sup>=128 years. The entire range is split in two halves: the range 0-63 is denoted by 0, 64-127 by 1. The operation is repeated with each subrange, i.e. 0-31 is 00, 32-63 is 01, 0-15 is 000, 32-47 is 010. Once completed, the age 30 will belong to the following ranges: 0 (0-63), 00 (0-31), 001 (16-31), 0011 (24-31), 00111 (28-31), 001111 (30-31), 0011110 (30). A 30 y.o. user is assigned a few tags to indicate the age, i.e. `age:00111`, `age:001111`, and `age:0011110`. Technically, all 7 tags may be assigned but usually it's impractical. To query for anyone in the age range 28-35 convert the range into a minimal number of tags: `age:00111` (28-31), `age:01000` (32-35). This query will match the 30 y.o. user by tag `age:00111`.

#### Query language

Tinode query language is used to define search queries for finding users and topics. The query is a string containing tags separated by spaces or commas. Tags are strings - individual query terms which are matched against user's or topic's tags. The tags can be written in an RTL language but the query as a whole is parsed left to right. Spaces are treated as the `AND` operator, commas (as well commas preceeded and/or followed by a space) as the `OR` operator. The order of operators is ignored: all `AND` operators are grouped together, all `OR` operators are grouped together. `OR` takes precedence over `AND`.
Expand All @@ -781,6 +770,17 @@ Tags containing spaces or commas must be enclosed in double quotes `"`, `\u0022`
* `flowers travel, puppies`: find topics or users which contain `flowers` and either `travel` or `puppies`, i.e. `flowers AND (travel OR puppies)`.


#### Some use cases
* Restricting users to organizations.
An immutable tag(s) may be assigned to the user which denotes the organization the user belons to. When the user searches for other users or topics, the search can be restricted to always contain the tag. This approach can be used to segment users into organizations with limited visiblity into each other.

* Search by geographical location.
Client software may periodically assign a [geohash](https://en.wikipedia.org/wiki/Geohash) tag to the user based on current location. Searching for users in a given area would mean matching on geohash tags.

* Search by numerical range, such as age range.
The approach is similar to geohashing. The entire range of numbers is covered by the smallest possible power of 2, for instance the range of human ages is covered by 2<sup>7</sup>=128 years. The entire range is split in two halves: the range 0-63 is denoted by 0, 64-127 by 1. The operation is repeated with each subrange, i.e. 0-31 is 00, 32-63 is 01, 0-15 is 000, 32-47 is 010. Once completed, the age 30 will belong to the following ranges: 0 (0-63), 00 (0-31), 001 (16-31), 0011 (24-31), 00111 (28-31), 001111 (30-31), 0011110 (30). A 30 y.o. user is assigned a few tags to indicate the age, i.e. `age:00111`, `age:001111`, and `age:0011110`. Technically, all 7 tags may be assigned but usually it's impractical. To query for anyone in the age range 28-35 convert the range into a minimal number of tags: `age:00111` (28-31), `age:01000` (32-35). This query will match the 30 y.o. user by tag `age:00111`.


### Peer to Peer Topics

Peer to peer (P2P) topics represent communication channels between strictly two users. The name of the topic is different for each of the two participants. Each of them sees the name of the topic as the user ID of the other participant: `usr` followed by base64 URL-encoded ID of the user. For example, if two users `usrOj0B3-gSBSs` and `usrIU_LOVwRNsc` start a P2P topic, the first one will see it as `usrIU_LOVwRNsc`, the second as `usrOj0B3-gSBSs`. The P2P topic has no owner.
Expand Down
21 changes: 0 additions & 21 deletions server/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -1025,24 +1025,3 @@ func replyTopicDescBasic(sess *Session, topic string, get *MsgClientGet) {
sess.queueOut(&ServerComMessage{
Meta: &MsgServerMeta{Id: get.Id, Topic: get.Topic, Timestamp: &now, Desc: desc}})
}

// Parse topic access parameters
func parseTopicAccess(acs *MsgDefaultAcsMode, defAuth, defAnon types.AccessMode) (auth, anon types.AccessMode,
err error) {

auth, anon = defAuth, defAnon

if acs.Auth != "" {
if err = auth.UnmarshalText([]byte(acs.Auth)); err != nil {
log.Println("hub: invalid default auth access mode '" + acs.Auth + "'")
}
}

if acs.Anon != "" {
if err = anon.UnmarshalText([]byte(acs.Anon)); err != nil {
log.Println("hub: invalid default anon access mode '" + acs.Anon + "'")
}
}

return
}
88 changes: 41 additions & 47 deletions server/topic.go
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,9 @@ func (t *Topic) replySetDesc(sess *Session, set *MsgClientSet) error {
now := types.TimeNow()

assignAccess := func(upd map[string]interface{}, mode *MsgDefaultAcsMode) error {
if mode == nil {
return nil
}
if auth, anon, err := parseTopicAccess(mode, types.ModeUnset, types.ModeUnset); err != nil {
return err
} else if auth.IsOwner() || anon.IsOwner() {
Expand Down Expand Up @@ -1318,32 +1321,22 @@ func (t *Topic) replySetDesc(sess *Session, set *MsgClientSet) error {
return
}

updateCached := func(upd map[string]interface{}) {
if tmp, ok := upd["Access"]; ok {
access := tmp.(types.DefaultAccess)
t.accessAuth = access.Auth
t.accessAnon = access.Anon
}
if public, ok := upd["Public"]; ok {
t.public = public
}
}

var err error
var sendPres bool

user := make(map[string]interface{})
topic := make(map[string]interface{})
// Change to the main object
core := make(map[string]interface{})
// Change to subscription
sub := make(map[string]interface{})
if set.Desc != nil {
if t.cat == types.TopicCatMe {
// Update current user
if set.Desc.DefaultAcs != nil {
err = assignAccess(user, set.Desc.DefaultAcs)
}
if set.Desc.Public != nil {
sendPres = assignGenericValues(user, "Public", set.Desc.Public)
}
err = assignAccess(core, set.Desc.DefaultAcs)
sendPres = assignGenericValues(core, "Public", set.Desc.Public)
} else if t.cat == types.TopicCatFnd {
// set.Desc.DefaultAcs is ignored.
// Do not send presence if fnd.Public has changed.
assignGenericValues(core, "Public", set.Desc.Public)
} else if t.cat == types.TopicCatP2P {
// Reject direct changes to P2P topics.
if set.Desc.Public != nil || set.Desc.DefaultAcs != nil {
Expand All @@ -1352,40 +1345,34 @@ func (t *Topic) replySetDesc(sess *Session, set *MsgClientSet) error {
}
} else if t.cat == types.TopicCatGrp {
// Update group topic
if set.Desc.DefaultAcs != nil || set.Desc.Public != nil {
if t.owner == sess.uid {
if set.Desc.DefaultAcs != nil {
err = assignAccess(topic, set.Desc.DefaultAcs)
}
if set.Desc.Public != nil {
sendPres = assignGenericValues(topic, "Public", set.Desc.Public)
}
} else {
// This is a request from non-owner
sess.queueOut(ErrPermissionDenied(set.Id, set.Topic, now))
return errors.New("attempt to change public or permissions by non-owner")
}
if t.owner == sess.uid {
err = assignAccess(core, set.Desc.DefaultAcs)
sendPres = assignGenericValues(core, "Public", set.Desc.Public)
} else if set.Desc.DefaultAcs != nil || set.Desc.Public != nil {
// This is a request from non-owner
sess.queueOut(ErrPermissionDenied(set.Id, set.Topic, now))
return errors.New("attempt to change public or permissions by non-owner")
}
}
// else fnd: update ignored

if err != nil {
sess.queueOut(ErrMalformed(set.Id, set.Topic, now))
return err
}

if set.Desc.Private != nil {
assignGenericValues(sub, "Private", set.Desc.Private)
}
sendPres = sendPres || assignGenericValues(sub, "Private", set.Desc.Private)
}

var change int
if len(user) > 0 {
err = store.Users.Update(sess.uid, user)
change++
}
if err == nil && len(topic) > 0 {
err = store.Topics.Update(t.name, topic)
if len(core) > 0 {
if t.cat == types.TopicCatMe {
err = store.Users.Update(sess.uid, core)
} else if t.cat == types.TopicCatFnd {
// The only value is Public, and Public for fnd is not saved according to specs.
} else {
err = store.Topics.Update(t.name, core)
}
// Change only affects message set to user: NoErr or InfoNotModified.
change++
}
if err == nil && len(sub) > 0 {
Expand All @@ -1402,16 +1389,23 @@ func (t *Topic) replySetDesc(sess *Session, set *MsgClientSet) error {
}

// Update values cached in the topic object
if t.cat == types.TopicCatMe || t.cat == types.TopicCatGrp {
if tmp, ok := core["Access"]; ok {
access := tmp.(types.DefaultAccess)
t.accessAuth = access.Auth
t.accessAnon = access.Anon
}
if public, ok := core["Public"]; ok {
t.public = public
}
} else if t.cat == types.TopicCatFnd {
// Assign per-session public.
}
if private, ok := sub["Private"]; ok {
pud := t.perUser[sess.uid]
pud.private = private
t.perUser[sess.uid] = pud
}
if t.cat == types.TopicCatMe {
updateCached(user)
} else if t.cat == types.TopicCatGrp {
updateCached(topic)
}

if sendPres {
// t.Public has changed, make an announcement
Expand Down
16 changes: 16 additions & 0 deletions server/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,22 @@ func getDefaultAccess(cat types.TopicCat, auth bool) types.AccessMode {
}
}

// Parse topic access parameters
func parseTopicAccess(acs *MsgDefaultAcsMode, defAuth, defAnon types.AccessMode) (auth, anon types.AccessMode,
err error) {

auth, anon = defAuth, defAnon

if acs.Auth != "" {
err = auth.UnmarshalText([]byte(acs.Auth))
}
if acs.Anon != "" {
err = anon.UnmarshalText([]byte(acs.Anon))
}

return
}

// Parses version of format 0.13.xx or 0.13-xx or 0.13
// The major and minor parts must be valid, the last part is ignored if missing or unparceable.
func parseVersion(vers string) int {
Expand Down

0 comments on commit ca306d7

Please sign in to comment.