186 lines
4.9 KiB
Go
186 lines
4.9 KiB
Go
// Package common contains utility methods used by all adapters.
|
|
package common
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tinode/chat/server/auth"
|
|
"github.com/tinode/chat/server/store"
|
|
t "github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
type AuthRecord struct {
|
|
Unique string `json:"unique" bson:"_id"`
|
|
UserId string `json:"userid"`
|
|
Scheme string `json:"scheme"`
|
|
AuthLvl auth.Level `json:"authLvl"`
|
|
Secret []byte `json:"secret"`
|
|
Expires time.Time `json:"expires"`
|
|
}
|
|
|
|
// SelectEarliestUpdatedSubs selects no more than the given number of subscriptions from the
|
|
// given slice satisfying the query. When the number of subscriptions is greater than the limit,
|
|
// the subscriptions with the earliest timestamp are selected.
|
|
func SelectEarliestUpdatedSubs(subs []t.Subscription, opts *t.QueryOpt, maxResults int) []t.Subscription {
|
|
limit := maxResults
|
|
ims := time.Time{}
|
|
if opts != nil {
|
|
if opts.Limit > 0 && opts.Limit < limit {
|
|
limit = opts.Limit
|
|
}
|
|
if opts.IfModifiedSince != nil {
|
|
ims = *opts.IfModifiedSince
|
|
}
|
|
}
|
|
|
|
// No cache management and the number of results is below the limit: return all.
|
|
if ims.IsZero() && len(subs) <= limit {
|
|
return subs
|
|
}
|
|
|
|
// Now that we fetched potentially more subscriptions than needed, we got to take those with the oldest modifications.
|
|
// Sorting in ascending order by modification time.
|
|
sort.Slice(subs, func(i, j int) bool {
|
|
return subs[i].LastModified().Before(subs[j].LastModified())
|
|
})
|
|
|
|
if !ims.IsZero() {
|
|
// Keep only those subscriptions which are newer than ims.
|
|
at := sort.Search(len(subs), func(i int) bool {
|
|
return subs[i].LastModified().After(ims)
|
|
})
|
|
subs = subs[at:]
|
|
}
|
|
// Trim slice at the limit.
|
|
if len(subs) > limit {
|
|
subs = subs[:limit]
|
|
}
|
|
|
|
return subs
|
|
}
|
|
|
|
// SelectLatestTime picks the latest update timestamp out of the two.
|
|
func SelectLatestTime(t1, t2 time.Time) time.Time {
|
|
if t1.Before(t2) {
|
|
// Subscription has not changed recently, use user's update timestamp.
|
|
return t2
|
|
}
|
|
|
|
return t1
|
|
}
|
|
|
|
// RangesToSql converts a slice of ranges to SQL BETWEEN or IN() constraint and arguments.
|
|
func RangesToSql(in []t.Range) (string, []any) {
|
|
if len(in) > 1 || in[0].Hi == 0 {
|
|
var args []any
|
|
for _, r := range in {
|
|
if r.Hi == 0 {
|
|
args = append(args, r.Low)
|
|
} else {
|
|
for i := r.Low; i < r.Hi; i++ {
|
|
args = append(args, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
return "IN (?" + strings.Repeat(",?", len(args)-1) + ")", args
|
|
}
|
|
|
|
// Optimizing for a special case of single range low..hi.
|
|
// SQL's BETWEEN is inclusive-inclusive thus decrement Hi by 1.
|
|
return "BETWEEN ? AND ?", []any{in[0].Low, in[0].Hi - 1}
|
|
}
|
|
|
|
// DisjunctionSql converts a slice of disjunctions to SQL HAVING clause and arguments.
|
|
func DisjunctionSql(req [][]string, fieldName string) (string, []any) {
|
|
var args []any
|
|
counts := make([]string, 0, len(req))
|
|
for _, reqDisjunction := range req {
|
|
// At least one of the tags must be present.
|
|
if len(reqDisjunction) == 0 {
|
|
continue
|
|
}
|
|
counts = append(counts, "COUNT("+fieldName+" IN (?"+strings.Repeat(",?", len(reqDisjunction)-1)+") OR NULL)>=1")
|
|
for _, tag := range reqDisjunction {
|
|
args = append(args, tag)
|
|
}
|
|
}
|
|
return "HAVING " + strings.Join(counts, " AND ") + " ", args
|
|
}
|
|
|
|
// FilterFoundTags keeps only those tags in setTags that are present in the index.
|
|
func FilterFoundTags(setTags t.StringSlice, index map[string]struct{}) []string {
|
|
foundTags := make([]string, 0, 1)
|
|
for _, tag := range setTags {
|
|
if _, ok := index[tag]; ok {
|
|
foundTags = append(foundTags, tag)
|
|
}
|
|
}
|
|
return foundTags
|
|
}
|
|
|
|
// Convert to JSON before storing to JSON field.
|
|
func ToJSON(src any) []byte {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
|
|
jval, _ := json.Marshal(src)
|
|
return jval
|
|
}
|
|
|
|
// Deserialize JSON data from DB.
|
|
func FromJSON(src any) any {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
if bb, ok := src.([]byte); ok {
|
|
var out any
|
|
json.Unmarshal(bb, &out)
|
|
return out
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Convert update to a list of columns and arguments.
|
|
func UpdateByMap(update map[string]any) (cols []string, args []any) {
|
|
for col, arg := range update {
|
|
col = strings.ToLower(col)
|
|
if col == "public" || col == "trusted" || col == "private" || col == "aux" {
|
|
arg = ToJSON(arg)
|
|
}
|
|
cols = append(cols, col+"=?")
|
|
args = append(args, arg)
|
|
}
|
|
return
|
|
}
|
|
|
|
// If Tags field is updated, get the tags so tags table cab be updated too.
|
|
func ExtractTags(update map[string]any) []string {
|
|
var tags []string
|
|
|
|
if val := update["Tags"]; val != nil {
|
|
tags, _ = val.(t.StringSlice)
|
|
}
|
|
|
|
return []string(tags)
|
|
}
|
|
|
|
// EncodeUidString takes decoded string representation of int64, produce UID.
|
|
// UIDs are stored as decoded int64 values.
|
|
func EncodeUidString(str string) t.Uid {
|
|
unum, _ := strconv.ParseInt(str, 10, 64)
|
|
return store.EncodeUid(unum)
|
|
}
|
|
|
|
// DecodeUidString takes UID as string, converts it to int64 representation.
|
|
// UIDs are stored as decoded int64 values.
|
|
func DecodeUidString(str string) int64 {
|
|
uid := t.ParseUid(str)
|
|
return store.DecodeUid(uid)
|
|
}
|