瀏覽代碼

tinode提供的服务

guoyachao 1 年之前
父節點
當前提交
b26724d2e5

+ 17 - 0
go.mod

@@ -0,0 +1,17 @@
+module jjl-tools
+
+go 1.19
+
+require (
+	github.com/go-redis/redis v6.15.9+incompatible
+	github.com/jinzhu/gorm v1.9.16
+	github.com/tinode/snowflake v1.0.0
+	golang.org/x/crypto v0.9.0
+)
+
+require (
+	github.com/go-sql-driver/mysql v1.5.0 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/onsi/ginkgo v1.16.5 // indirect
+	github.com/onsi/gomega v1.27.8 // indirect
+)

+ 34 - 0
tinodDemo.go

@@ -0,0 +1,34 @@
+package main
+
+import (
+	"fmt"
+	"jjl-tools/tinodeService/config"
+	. "jjl-tools/tinodeService/service"
+)
+
+func main() {
+	//数据库初始化
+	conf :=config.Conf{
+		DbConnect:"dev_jjlrednet:FFH5cv94l3j5gk98@(192.168.204.180:3377)/tinode?charset=utf8&parseTime=True&loc=Local",//数据库配置
+		RedisConnect:"192.168.204.183:6379",//redis配置
+	}
+	conf.Init()
+
+	//通过chatId获取用户的所有的会话列表
+	sub:=new(SubscriptionsService)
+	mesList,_:=sub.GetSubsCoutomerListByChatId("22RymOGBDBw")
+	fmt.Println("===mesList==", mesList)
+
+	//通过会话topic 查看聊记录
+	topicService := Topics{
+		Topic:      "p2pvUxuIC8HthonmVuGVFAh0A",//会话标题
+		Sid:        "",//会话chatid
+		CreateTime: "",
+		PageSize:   10,
+	}
+	list,_:=topicService.GetMessageList()
+	fmt.Println("===list==", list)
+	//获取会话开始时间, 结束时间
+	timeLists :=topicService.GetChatRecordStartEndTime()
+	fmt.Println("===timeLists==", timeLists)
+}

+ 12 - 0
tinodeService/config/conf.go

@@ -0,0 +1,12 @@
+package config
+
+type Conf struct {
+	DbConnect    string //"dev_jjlrednet:FFH5cv94l3j5gk98@(192.168.204.180:3377)/tinode?charset=utf8&parseTime=True&loc=Local"
+	RedisConnect string //192.168.204.183:6379
+}
+
+var GlobalConfig *Conf
+
+func (conf *Conf) Init() {
+	GlobalConfig = conf
+}

+ 40 - 0
tinodeService/dao/LeadsDao.go

@@ -0,0 +1,40 @@
+package dao
+
+import (
+	. "jjl-tools/tinodeService/lib"
+)
+
+type Leads struct {
+	LeadsId         int    `json:"leads_id"`
+	Name            string `json:"name"`           // 线索名称
+	Source          string `json:"source"`         // 线索来源
+	Telephone       string `json:"telephone"`      // 电话
+	Mobile          string `json:"mobile"`         // 手机
+	Industry        string `json:"industry"`       // 客户行业
+	Remark          string `json:"remark"`         // 备注
+	CreateUserId    string `json:"create_user_id"` // 创建人ID
+	OwnerUserId     string `json:"owner_user_id"`
+	CreateTime      string `json:"create_time"` // 创建时间
+	UpdateTime      string `json:"update_time"` // 更新时间
+	WeChat          string `json:"we_chat"`
+	Qq              string `json:"qq"`
+	Ext1            string `json:"ext1"`
+	Ext2            string `json:"ext2"`
+	Uid             string `json:"uid"`
+	MobileEncrypted string `json:"mobile_encrypted"`
+	MobileMask      string `json:"mobile_mask"`
+	City            int    `json:"city"`
+	Country         int    `json:"country"`
+}
+type LeadsDao struct {
+}
+func (ra *LeadsDao) TableName() string {
+	return "leads"
+}
+
+//info
+func (ra *LeadsDao) GetLeadsInfo(maps map[string]interface{}) (Leads, error) {
+	var leads Leads
+	err := GetTiNodeDbInstance().Table(ra.TableName()).Where(maps).Find(&leads).Error
+	return leads, err
+}

+ 228 - 0
tinodeService/dao/MessagesDao.go

@@ -0,0 +1,228 @@
+package dao
+
+import (
+	"encoding/json"
+	"github.com/jinzhu/gorm"
+	. "jjl-tools/tinodeService/lib"
+	"strconv"
+	"strings"
+	"time"
+)
+
+type MessageDao struct {
+}
+
+type Messages struct {
+	Createdat time.Time `json:"createdat"`
+	Updatedat string    `json:"updatedat"`
+	Topic     string    `json:"topic"`
+	From      int64     `json:"from"`
+	Realname  string    `json:"realname"`
+	Avatar    string    `json:"avatar"`
+	Content   string    `json:"content"`
+	Role      int       `json:"role"`
+	Head      string    `json:"head"`
+	ChatId    string    `json:"chat_id"`
+}
+type ChatUser struct {
+	Realname string `json:"realname"`
+	Avatar   string `json:"avatar"`
+	Uid      int    `json:"uid"`
+}
+type Members struct {
+	Id         int
+	Uid        int
+	ChatUid    int
+	Nickname   string
+	Avatar     string
+	CreateTime string
+}
+type InspectionDao struct {
+}
+
+func (insdao *InspectionDao) getUserFromDB(role int, uid int64) (ChatUser, error) {
+	var member ChatUser
+	var sql string
+	if role == 1 {
+		sql = "SELECT realname FROM `go_user` WHERE chat_uid = ?"
+	} else {
+		sql = "SELECT nickname AS realname, avatar FROM `members` WHERE chat_uid=? "
+	}
+	query := GetTiNodeDbInstance().Debug().Raw(sql, uid).Select("realname")
+	err := query.Find(&member).Error
+	if err == gorm.ErrRecordNotFound {
+		err = nil
+	}
+	return member, nil
+}
+func (insdao *InspectionDao) GetUserFromRedis(role int, uid int64) (ChatUser, error) {
+	var customer ChatUser
+	var member Members
+	ccd := InspectionDao{}
+	//获取客服名字
+	if role == 1 {
+		key := "customer:info:" + strconv.FormatInt(uid, 10)
+		redisCli := GetRedisInstance().Connect(1)
+		info, err := redisCli.Get(key).Result()
+		if err != nil {
+			customer, errs := ccd.getUserFromDB(1, uid)
+			if customer.Realname == "" || errs != nil {
+				customer.Realname = "无此客服"
+			}
+			return customer, nil
+		}
+		err = json.Unmarshal([]byte(info), &customer)
+		if err != nil {
+			customer.Realname = "无此客服"
+		}
+		return customer, nil
+	} else {
+		leadsDao := LeadsDao{}
+		//获取用户信息
+		var mcacheKey = "member:info:*:" + strconv.FormatInt(uid, 10)
+		uidStr := strconv.FormatInt(uid, 10)
+		redis := GetRedisInstance().Connect(2)
+		cacheKey := redis.Keys(mcacheKey).Val()
+		if len(cacheKey) == 0 {
+			//读库
+			customer, errs := ccd.getUserFromDB(2, uid)
+			if customer.Realname == "" || errs != nil {
+				customer.Realname = UidSEncodeNoErr(uidStr)
+			}
+			//再从leads表获取用户名
+			leadsInfo, _ := leadsDao.GetLeadsInfo(map[string]interface{}{"uid": uid})
+			if leadsInfo.Name != "" {
+				customer.Realname = leadsInfo.Name
+			}
+			return customer, nil
+		}
+
+		info, _ := redis.Get(cacheKey[0]).Result()
+
+		json.Unmarshal([]byte(info), &member)
+		customer.Realname = member.Nickname
+		customer.Avatar = member.Avatar
+		if member.Nickname == "" {
+			customer.Realname = UidSEncodeNoErr(uidStr)
+		}
+
+		//再从leads表获取用户名
+		leadsInfo, _ := leadsDao.GetLeadsInfo(map[string]interface{}{"uid": uid})
+		if leadsInfo.Name != "" {
+			customer.Realname = leadsInfo.Name
+		}
+		return customer, nil
+	}
+
+	return customer, nil
+}
+func (mesDao MessageDao) GetMessagesList(topic, sid, messageTime string, limit int) (mesList []Messages, err error) {
+	res := GetTiNodeDbInstance().Debug().Table("messages").Joins("left join dellog on dellog.topic = messages.topic AND messages.seqid = dellog.low").Where("messages.deletedat is null AND messages.delid = 0 AND dellog.deletedfor IS NULL")
+	if topic != "" {
+		res = res.Where("messages.topic = ?", topic)
+	}
+	if sid != "" {
+		res = res.Where("messages.chat_id = ?", sid)
+	}
+	if messageTime != "" {
+		res = res.Where("messages.createdat < ?", messageTime)
+	}
+	res = res.Order("messages.id DESC").Limit(limit).Find(&mesList)
+	if err != nil {
+		return nil, err
+	}
+	ccd := InspectionDao{}
+	var customer ChatUser
+	for k, v := range mesList {
+		customer.Realname = ""
+		customer, _ := ccd.GetUserFromRedis(v.Role, v.From)
+		mesList[k].Realname = customer.Realname
+		mesList[k].Avatar = customer.Avatar
+		//聊天内容去掉开头和结尾的""
+		mesList[k].Content = strings.Trim(v.Content, "\"")
+	}
+	return
+}
+
+type ChatRecordDao struct {
+}
+
+type SemJsChatRecord struct {
+	CHAT_ID                int    `gorm:"column:CHAT_ID"`
+	SID                    string `gorm:"column:SID"`
+	CREATE_TIME            string `gorm:"column:CREATE_TIME"`       //创建时间
+	VISITOR_ID             string    `gorm:"column:VISITOR_ID"`        //访客ID
+	USER_ID                string    `gorm:"column:USER_ID"`           //客服ID
+	VISITOR_STATIC_ID      string `gorm:"column:VISITOR_STATIC_ID"` //访客唯一标识
+	INVITE_MODE            int64  `gorm:"column:INVITE_MODE"`
+	REFER_PAGE             string `gorm:"column:REFER_PAGE" json:"refer_page"` //来源地址
+	COMPANY_ID             int    `gorm:"column:COMPANY_ID"`
+	CHAT_URL               string `gorm:"column:CHAT_URL"`  //对话发起页
+	FIRST_URL              string `gorm:"column:FIRST_URL"` //着陆页
+	BIZ                    int    `gorm:"column:BIZ"`
+	CHANNLE_ID             int    `gorm:"column:CHANNLE_ID"`
+	CATEGORY_ID            int    `gorm:"column:CATEGORY_ID"`
+	END                    int    `gorm:"column:END"`
+	GrUserId               string `json:"gr_user_id"`                //growingIO生成的UserID标识符
+	Topic                  string `json:"topic"`                     //会话名
+	CustomerMsgNum         int    `json:"customer_msg_num"`          //客服发送消息数
+	UserMsgNum             int    `json:"user_msg_num"`              //用户发送消息数
+	UserFirstMsgTime       string `json:"user_first_msg_time"`       //用户第一条消息时间
+	CustomerFirstReplyTime string `json:"customer_first_reply_time"` //客服第一次回复时间
+	CallEndTime            string `json:"call_end_time"`
+	CallEndType            string `json:"call_end_type"`
+	Ip                     string `json:"ip"`
+	Seqid                  int    `json:"seqid"`             //消息总条数
+	Delid                  int    `json:"delid"`             //删除条数
+	Tags                   string `json:"tags"`              //标签
+	LeadsId                int    `json:"leads_id"`          //名片id
+	LeadsName              string `json:"leads_name"`        //名片名称
+	Telephone              string `json:"telephone"`         //电话
+	Mobile                 string `json:"mobile"`            //手机
+	Industry               string `json:"industry"`          //客户行业
+	Remark                 string `json:"remark"`            //备注
+	CreateUserId           string `json:"create_user_id"`    //创建人ID
+	OwnerUserId            string `json:"owner_user_id"`     //负责人ID
+	Uid                    string `json:"uid"`               //用户uid
+	MobileEncrypted        string `json:"mobile_encrypted""` //手机号加密
+	MobileMask             string `json:"mobile_mask"`       //手机号掩码
+	Country                int    `json:"country"`           //国家
+	City                   int    `json:"city"`              //城市
+	WeChat                 string `json:"we_chat"`           //微信
+	Qq                     string `json:"qq"`                //qq
+
+	CUid string `json:"cUid"`
+
+	GroupId   int    `json:"group_id"`   //技能组id
+	GroupName string `json:"group_name"` //技能组名称
+	Realname  string `json:"realname"`   //客服名称
+
+	ChannelName string `json:"channel_name"`
+	CategoryName string `json:"category_name"`
+
+	VisitorName string 	`json:"visitor_name"`
+}
+
+func (chatdao *ChatRecordDao) GetChatRecordByChatId(chatId string) (info SemJsChatRecord, err error) {
+	err = GetTiNodeDbInstance().Debug().Table("sem_js_chat_record").Where("SID = ?", chatId).First(&info).Error
+	if err != nil {
+		return
+	}
+	return
+}
+/**
+ * 获取sem_js_chat_record 详情
+ * @apiParam {string} topic
+ * @apiParam {string} sid
+ */
+func (chatdao *ChatRecordDao) GetGroupByChatRecordLists(topic, sid string) (list []SemJsChatRecord, err error) {
+	sql := GetTiNodeDbInstance().Debug().Table("sem_js_chat_record")
+	if topic != "" {
+		sql = sql.Where("topic = ?", topic)
+	}
+	if sid != "" {
+		sql = sql.Where("SID = ?", sid)
+	}
+	err = sql.Find(&list).Error
+	return
+}

+ 46 - 0
tinodeService/dao/SubscriptionsDao.go

@@ -0,0 +1,46 @@
+package dao
+
+import (
+	"fmt"
+	. "jjl-tools/tinodeService/lib"
+	"time"
+)
+
+type ISubscriptionsDao interface {
+}
+
+type SubscriptionsDao struct {
+}
+
+type Subscriptions struct {
+	ID        int    `json:"id"`
+	Userid    int64  `json:"userid"`
+	Topic     string `json:"topic"`
+	From     string `json:"from"`   //返回用户的usr 例如: usrxxx, 不是数据库表字段
+	Recvseqid int    `json:"recvseqid"`
+	Readseqid int    `json:"readseqid"`
+	Star      int    `json:"star"`
+	Avatar    string `json:"avatar"`
+	Nickname  string `json:"nickname"`
+	Status    int    `json:"status"`
+	Createdat string `json:"createdat"`
+	Updatedat time.Time `json:"updatedat"`
+
+	OsEnd     string `json:"os_end"`
+}
+type Subs struct {
+	Subscriptions
+	Updateat time.Time `json:"updateat"`    //根据用户uid获取所有的客服sub,这里需要去2个时间做对比,获得最后的聊天时间
+}
+//根据用户uid获取所有的客服sub
+func (subDao SubscriptionsDao) GetSubsCoutomerList(uid string) (subList []Subs, err error) {
+	res := GetTiNodeDbInstance().Debug().Select("s.id, s.userid, s.topic, s.recvseqid, s.readseqid, s.star, s.createdat, s.updatedat, u.realname as nickname, u.avatar_file, u.status, sub.updatedat as updateat").
+		Table("subscriptions s").Joins("LEFT JOIN subscriptions sub ON sub.topic = s.topic").
+		Joins("LEFT JOIN go_user u ON u.chat_uid = s.userid").
+		Where("s.deletedat is null AND u.status = 0 AND u.deleted_on = 0 AND sub.userid = ?", uid)
+	res = res.Order("s.id desc").Find(&subList)
+	if err = res.Error; err != nil {
+		fmt.Println("===GetSubsCoutomerList=====",err)
+	}
+	return
+}

+ 37 - 0
tinodeService/lib/db.go

@@ -0,0 +1,37 @@
+package lib
+
+import (
+	. "jjl-tools/tinodeService/config"
+	"github.com/jinzhu/gorm"
+	_ "github.com/jinzhu/gorm/dialects/mysql"
+	"log"
+	"sync"
+)
+
+var dblock *sync.Mutex = &sync.Mutex{}
+
+var tiDbClient *gorm.DB
+var esOnce sync.Once
+
+//GetTiNodeDbInstance 数据库连接实例
+func GetTiNodeDbInstance() *gorm.DB {
+	var err error
+	esOnce.Do(func() {
+		config := GlobalConfig
+		tiDbClient, err = gorm.Open("mysql", config.DbConnect)
+		if err != nil {
+			log.Panic(err.Error())
+		}
+		//设置闲置的连接数
+		tiDbClient.DB().SetMaxIdleConns(10)
+		//设置最大打开的连接数,默认值为0表示不限制
+		tiDbClient.DB().SetMaxOpenConns(100)
+		//如果设置为true,`User`的默认表名为`user`,使用`TableName`设置的表名不受影响
+		tiDbClient.SingularTable(true)
+	})
+	return tiDbClient
+}
+
+func ColumnIncr(column string) interface{} {
+	return gorm.Expr(column+" + ?", 1)
+}

+ 230 - 0
tinodeService/lib/globalFunc.go

@@ -0,0 +1,230 @@
+package lib
+
+import (
+	"jjl-tools/tinodeService/store/types"
+	"encoding/binary"
+	"encoding/json"
+	"fmt"
+	"golang.org/x/crypto/xtea"
+	"io/ioutil"
+	"net/http"
+	"reflect"
+	"runtime"
+	"strconv"
+	"strings"
+	"unsafe"
+)
+
+func SizeStruct(data interface{}) int {
+	return sizeof(reflect.ValueOf(data))
+}
+//uid加密函数 -- 客户端使用,服务端处理后返回给客户端的uid需要做加密处理
+func GencodeUid(val int64) string {
+	var src = make([]byte, 8)
+	var dst = make([]byte, 8)
+	binary.LittleEndian.PutUint64(src, uint64(val))
+	var cipher *xtea.Cipher
+	cipher, _ = xtea.NewCipher(getUidKey())
+	cipher.Encrypt(dst, src)
+	user := types.Uid(binary.LittleEndian.Uint64(dst))
+	return user.UserId()
+}
+//字符串  622919054444728320 转成usr这种
+func UidSEncodeNoErr(uid string) string {
+	if uid == "" {
+		return ""
+	}
+	if strings.Contains(uid, "usr") {
+		return uid
+	}
+	uidI, err := strconv.ParseInt(uid, 10, 64)
+	if err != nil {
+		//logger.Error(gsfn, "fatal err", err, "uid", uid)
+		return ""
+	}
+	return encodeUid(uidI)
+}
+//uid加密函数 -- 客户端使用,服务端处理后返回给客户端的uid需要做加密处理
+func encodeUid(val int64) string {
+	var src = make([]byte, 8)
+	var dst = make([]byte, 8)
+	binary.LittleEndian.PutUint64(src, uint64(val))
+	var cipher *xtea.Cipher
+	cipher, _ = xtea.NewCipher(getUidKey())
+	cipher.Encrypt(dst, src)
+	user := types.Uid(binary.LittleEndian.Uint64(dst))
+	return user.UserId()
+}
+
+func sizeof(v reflect.Value) int {
+	switch v.Kind() {
+	case reflect.Map:
+		sum := 0
+		keys := v.MapKeys()
+		for i := 0; i < len(keys); i++ {
+			mapkey := keys[i]
+			s := sizeof(mapkey)
+			if s < 0 {
+				return -1
+			}
+			sum += s
+			s = sizeof(v.MapIndex(mapkey))
+			if s < 0 {
+				return -1
+			}
+			sum += s
+		}
+		return sum
+	case reflect.Slice, reflect.Array:
+		sum := 0
+		for i, n := 0, v.Len(); i < n; i++ {
+			s := sizeof(v.Index(i))
+			if s < 0 {
+				return -1
+			}
+			sum += s
+		}
+		return sum
+
+	case reflect.String:
+		sum := 0
+		for i, n := 0, v.Len(); i < n; i++ {
+			s := sizeof(v.Index(i))
+			if s < 0 {
+				return -1
+			}
+			sum += s
+		}
+		return sum
+
+	case reflect.Ptr, reflect.Interface:
+		p := (*[]byte)(unsafe.Pointer(v.Pointer()))
+		if p == nil {
+			return 0
+		}
+		return sizeof(v.Elem())
+	case reflect.Struct:
+		sum := 0
+		for i, n := 0, v.NumField(); i < n; i++ {
+			s := sizeof(v.Field(i))
+			if s < 0 {
+				return -1
+			}
+			sum += s
+		}
+		return sum
+
+	case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
+		reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+		reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128,
+		reflect.Int:
+		return int(v.Type().Size())
+
+	default:
+		//fmt.Println("t.Kind() no found:", v.Kind())
+	}
+
+	return -1
+}
+
+/*
+* 返回函数名
+ */
+func GetSysFuncName() string {
+	pc, _, line, _ := runtime.Caller(1)
+	f := runtime.FuncForPC(pc)
+	res := "line:" + strconv.Itoa(line) + "||funcName:" + f.Name() + "||error:"
+	return res
+}
+
+// type Uid uint64
+type testJose struct {
+	UserKey string `json:"uid_key"`
+}
+
+type configType struct {
+	// 16-byte key for XTEA. Used to initialize types.UidGenerator.
+	UidKey []byte `json:"uid_key"`
+}
+
+func getUidKey() []byte {
+	test := testJose{UserKey: "la6YsO+bNX/+XIkOqc5Svw=="}
+	jsonStr, err := json.Marshal(test)
+	var config configType
+	err = json.Unmarshal([]byte(jsonStr), &config)
+	if err != nil {
+		fmt.Println(err)
+	}
+	return config.UidKey
+}
+
+type AccessTokenResp struct {
+	Code        int    `json:"code"`
+	Errmsg      string `json:"errmsg"`
+	AccessToken string `json:"access_token"`
+	ExpiresIn   string `json:"expires_in"`
+}
+
+
+func RequestOnlineMap(url string) map[string]interface{} {
+	client := http.Client{}
+	req, err := http.NewRequest(http.MethodGet, url, nil)
+	if err != nil {
+		fmt.Println("err 320 line: ", err)
+	}
+	// 添加请求头
+	req.Header.Add("Content-type", "application/json;charset=utf-8")
+	// 发送请求
+	resp, err := client.Do(req)
+	if err != nil {
+		fmt.Println("err 327 line: ", err)
+	}
+	defer resp.Body.Close()
+	b, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("err 332 line: ", err)
+	}
+	//fmt.Println(string(b))
+
+	resMap := make(map[string]interface{})
+	err = json.Unmarshal(b, &resMap)
+	return resMap
+}
+func Interface2Int(val interface{}) int {
+	var res int
+	res = 0
+	switch val.(type) {
+	case string:
+		res, _ = strconv.Atoi(val.(string))
+		break
+	case int:
+		res = val.(int)
+		break
+	case int64:
+		tmp := strconv.FormatInt(val.(int64), 10)
+		res, _ = strconv.Atoi(tmp)
+		break
+	case uint16:
+		res = int(val.(uint16))
+		break
+	}
+
+	return res
+}
+func Interface2String(val interface{}) string {
+	var res string
+	res = ""
+	switch val.(type) {
+	case int:
+		res = strconv.Itoa(val.(int))
+		break
+	case string:
+		res = val.(string)
+		break
+	case int64:
+		res = strconv.FormatInt(val.(int64), 10)
+		break
+	}
+	return res
+}
+

+ 44 - 0
tinodeService/lib/redis.go

@@ -0,0 +1,44 @@
+package lib
+
+import (
+	. "jjl-tools/tinodeService/config"
+	"sync"
+
+	"github.com/go-redis/redis"
+)
+
+type Redis struct{}
+
+var redisClient *Redis
+var redisDb map[int]*redis.Client
+var redisOnce sync.Once
+var redislock *sync.Mutex = &sync.Mutex{}
+
+//create obj
+func GetRedisInstance() *Redis {
+	redisOnce.Do(func() {
+		if len(redisDb) == 0 {
+			redisDb = make(map[int]*redis.Client)
+		}
+		redisClient = &Redis{}
+	})
+	return redisClient
+}
+
+//action
+func (Redis) Connect(db int) *redis.Client {
+	//double check pattern
+	if redisDb[db] == nil {
+		redislock.Lock()
+		defer redislock.Unlock()
+
+		if redisDb[db] == nil {
+			redisDb[db] = redis.NewClient(&redis.Options{
+				Addr:     GlobalConfig.RedisConnect,
+				Password: "", // no password set
+				DB:       db, // use default DB
+			})
+		}
+	}
+	return redisDb[db]
+}

+ 106 - 0
tinodeService/service/CommonService.go

@@ -0,0 +1,106 @@
+package service
+
+const (
+	ErrOk                 = 0
+	ErrNeedParam          = 1
+	ErrParamRange         = 14
+	ErrCompany            = 1000
+	ErrAllocateFail       = 1001
+	ErrCompanyParam       = 1002
+	ErrRouteTag           = 1003
+	ErrNeedUid            = 1004
+	ErrSuitRote           = 1005
+	ErrTGid               = 1006
+	ErrInParam            = 1007
+	ErrQueryError         = 1008
+	ErrCustomerNotFind    = 2001
+	ErrCustomerNotOnline  = 2002
+	ErrCustomerQueue      = 2004
+	ErrQueueEmpty         = 2005
+	ErrPhone              = 1010003
+	ErrPhoneExist         = 4
+	ErrAddDataFail        = 1010005
+	ErrMaxReceiveNum      = 2006
+	ErrBlackList          = 2007
+	ErrAddFail            = 2008
+	ErrUpdateFail         = 2009
+	ErrEmptyDataError     = 2010
+	ErrMaxReceiveNumLimit = 2011
+	ErrMaxRecoveryNum     = 2012
+	ErrNameLen            = 1010004
+	ErrNeedLen            = 1010006
+	ErrGroupNotFind       = 3001
+	ErrGroupOnline        = 3002
+	ErrParamError         = 28
+	ErrParamCheck         = 5001
+	ErrTokenCheck         = 20001
+	ErrUserNotFind        = 5003
+	ErrServer             = 5004
+	ErrExist              = 5005
+	ErrNotExist           = 5006
+	ErrTokenExpired       = 20002
+	Err                   = 20003
+	ErrDataTooLong        = 1406
+	ErrData               = 2
+	ErrAllocation         = 20013
+	ErrProp               = 20014
+	ErrTopicInfo          = 20015
+
+	ERROR_SIGN_FAIL       = 30001
+)
+
+var errCodeMap = map[int]string{
+	ErrDataTooLong:        "数据长度超过存储限制",
+	ErrOk:                 "请求成功",
+	ErrNeedParam:          "缺少参数!",
+	ErrParamRange:         "参数范围错误",
+	ErrNeedUid:            "缺少uid",
+	ErrCompany:            "获取公司路由分组错误",
+	ErrCompanyParam:       "公司参数错误",
+	ErrSuitRote:           "没有符合条件的路由",
+	ErrPhoneExist:         "手机号已经存在",
+	ErrAddDataFail:        "数据添加失败",
+	ErrTGid:               "错误的技能组id",
+	ErrNameLen:            "输入姓名不能大于12字符!",
+	ErrNeedLen:            "输入内容不能大于500个字符",
+	ErrRouteTag:           "标签参数错误",
+	ErrInParam:            "参数错误",
+	ErrAllocateFail:       "分配失败或者对应客服不在线", //客服不在线
+	ErrCustomerNotFind:    "客服不存在",
+	ErrCustomerQueue:      "排队中,请稍等",
+	ErrMaxReceiveNum:      "会话数达到最大接待数上线",
+	ErrBlackList:          "黑名单用户",
+	ErrQueueEmpty:         "当前没有用户在排队",
+	ErrCustomerNotOnline:  "顾问不在线",
+	ErrGroupNotFind:       "找不到符合条件的技能组",
+	ErrGroupOnline:        "技能组没有在线客服",
+	ErrAddFail:            "添加失败",
+	ErrUpdateFail:         "更新失败",
+	ErrQueryError:         "数据库查询失败",
+	ErrEmptyDataError:     "空记录",
+	ErrMaxReceiveNumLimit: "最大接待数设置上线为100",
+	ErrMaxRecoveryNum:     "快捷回复字数不能超过500字",
+	ErrParamCheck:         "参数不合法",
+	ErrTokenCheck:         "Token鉴权失败",
+	ErrUserNotFind:        "用户不存在",
+	ErrServer:             "服务端处理错误",
+	ErrExist:              "数据已存在",
+	ErrNotExist:           "数据不存在",
+	ErrParamError:         "参数错误",
+	ErrTokenExpired:       "Token已过期",
+	ErrData:               "暂无数据",
+	Err:                   "服务器错误",
+	ErrAllocation:         "分配失败",
+	ErrPhone:              "电话号码错误",
+	ErrProp:               "国家和公司找不到对应标签",
+	ERROR_SIGN_FAIL:       "签名错误",
+	ErrTopicInfo:		   "会话信息错误",
+}
+type SerError int
+
+func (e SerError) String() string {
+	if v, er := errCodeMap[int(e)]; er {
+		return v
+	}
+	return "未找到错误码,请检查错误码"
+}

+ 43 - 0
tinodeService/service/SubscriptionsService.go

@@ -0,0 +1,43 @@
+package service
+
+import (
+	"fmt"
+	. "jjl-tools/tinodeService/dao"
+	. "jjl-tools/tinodeService/lib"
+)
+
+type SubscriptionsService struct {
+}
+type SubscriptionsListReturn struct {
+	TotalCount int             `json:"total_count"`
+	Totalpage  int             `json:"totalpage"`
+	Page       int             `json:"page"`
+	List       []Subscriptions `json:"list"`
+}
+
+//通过chatId获取用户的所有的会话列表
+func (subSvc SubscriptionsService) GetSubsCoutomerListByChatId(chatId string) (subList []Subs, errCode int) {
+	errCode = ErrOk
+	//在ChatRecord中获取用户的uid
+	chatDao := ChatRecordDao{}
+	record, err :=chatDao.GetChatRecordByChatId(chatId)
+	if err != nil {
+		errCode = ErrData
+	}
+	fmt.Println("====record====",record)
+	//通过uid获取用户的所有的会话列表
+	subDao := SubscriptionsDao{}
+	subList, err = subDao.GetSubsCoutomerList(record.VISITOR_ID)
+	if err != nil {
+		errCode = ErrData
+	}
+
+	for k, v := range subList {
+		subList[k].From = GencodeUid(v.Userid)
+		//这里去最后的聊天时间, 取订阅subs的最后updatedat, 由于是2个订阅, 那个大取那个
+		if (v.Updatedat.Before(v.Updateat)) {
+			subList[k].Updatedat = v.Updateat
+		}
+	}
+	return
+}

+ 78 - 0
tinodeService/service/TopicService.go

@@ -0,0 +1,78 @@
+package service
+
+import (
+	"fmt"
+	. "jjl-tools/tinodeService/dao"
+	"strconv"
+)
+
+type Topics struct {
+	Id                 int
+	PageNum            int
+	PageSize           int
+	CompanyId          int
+	SkillGroups        string
+	GroupId            int
+	Uid                string
+	StartTime          string
+	EndTime            string
+	Name               string
+	Content            string
+	Quality            int
+	FirstReplyInterval int
+	//查询会话记录
+	Topic      string
+	Sid        string
+	CreateTime string
+	MsgNum     int
+	ChannelId  int
+	CategoryId int
+	IsReply int
+}
+
+//获取会话详细记录
+func (tp *Topics) GetMessageList() (mesList []Messages, err error) {
+	mesDao := MessageDao{}
+	//获取总条数
+	mesList, err = mesDao.GetMessagesList(tp.Topic, tp.Sid, tp.CreateTime, tp.PageSize)
+	return
+}
+//获取会话开始时间, 结束时间
+func (tp *Topics) GetChatRecordStartEndTime() map[string]interface{} {
+	chatDao := ChatRecordDao{}
+	chatList, err := chatDao.GetGroupByChatRecordLists(tp.Topic, tp.Sid)
+	if err != nil {
+		fmt.Println("数据为空或获取会话信息错误, topic:", tp.Topic, ", sid:", tp.Sid, ", err=", err)
+	}
+	insDao := InspectionDao{}
+	list := make(map[string]interface{})
+	endDes := map[string]string{
+		"1": "客服结束",
+		"2": "用户结束",
+		"3": "用户超时自动关闭",
+		"4": "客服异常关闭",
+		"5": "用户异常关闭",
+	}
+	for _, v := range chatList {
+		//判断 v.SID 是否数字, 默认是用户发送的 - 2, 如果是全数组则是客服发送的 - 1
+		call_start_type := 2
+		if _, err := strconv.ParseFloat(v.SID, 64); err == nil {
+			call_start_type = 1
+		}
+		vUser,_ := strconv.ParseInt(v.USER_ID, 10, 64)
+		vVistor,_ := strconv.ParseInt(v.VISITOR_ID, 10, 64)
+		customer, _ := insDao.GetUserFromRedis(1, vUser)
+		user, _ := insDao.GetUserFromRedis(2, vVistor)
+		cInfo := map[string]interface{}{
+			"call_start_time":   v.CREATE_TIME,
+			"call_end_time":     v.CallEndTime,
+			"call_end_type":     v.CallEndType,
+			"call_end_des":      endDes[v.CallEndType],
+			"call_start_type":   call_start_type,
+			"customer_nickname": customer.Realname, //客服昵称
+			"user_nickname":     user.Realname,     //用户昵称
+		}
+		list[v.SID] = cInfo
+	}
+	return list
+}

+ 52 - 0
tinodeService/store/store.go

@@ -0,0 +1,52 @@
+package store
+
+import (
+	"encoding/json"
+	"jjl-tools/tinodeService/store/types"
+)
+
+// Unique ID generator
+var uGen types.UidGenerator
+
+type configType struct {
+	// 16-byte key for XTEA. Used to initialize types.UidGenerator.
+	UidKey []byte `json:"uid_key"`
+	// Maximum number of results to return from adapter.
+	MaxResults int `json:"max_results"`
+	// Configurations for individual adapters.
+	Adapters map[string]json.RawMessage `json:"adapters"`
+}
+
+// GetUid generates a unique ID suitable for use as a primary key.
+func GetUid() types.Uid {
+	return uGen.Get()
+}
+
+// GetUidString generate unique ID as string
+func GetUidString() string {
+	return uGen.GetStr()
+}
+
+// DecodeUid takes an XTEA encrypted Uid and decrypts it into an int64.
+// This is needed for sql compatibility. Tte original int64 values
+// are generated by snowflake which ensures that the top bit is unset.
+func DecodeUid(uid types.Uid) int64 {
+	if uid.IsZero() {
+		return 0
+	}
+	return uGen.DecodeUid(uid)
+}
+
+// EncodeUid applies XTEA encryption to an int64 value. It's the inverse of DecodeUid.
+func EncodeUid(id int64) types.Uid {
+	if id == 0 {
+		return types.ZeroUid
+	}
+	return uGen.EncodeInt64(id)
+}
+
+// UsersObjMapper is a users struct to hold methods for persistence mapping for the User object.
+type UsersObjMapper struct{}
+
+// Users is the ancor for storing/retrieving User objects
+var Users UsersObjMapper

+ 1067 - 0
tinodeService/store/types/types.go

@@ -0,0 +1,1067 @@
+package types
+
+import (
+	"database/sql/driver"
+	"encoding/base32"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"sort"
+	"strings"
+	"time"
+)
+
+// StoreError satisfies Error interface but allows constant values for
+// direct comparison.
+type StoreError string
+
+// Error is required by error interface.
+func (s StoreError) Error() string {
+	return string(s)
+}
+
+const (
+	// ErrInternal means DB or other internal failure
+	ErrInternal = StoreError("internal")
+	// ErrMalformed means the secret cannot be parsed or otherwise wrong
+	ErrMalformed = StoreError("malformed")
+	// ErrFailed means authentication failed (wrong login or password, etc)
+	ErrFailed = StoreError("failed")
+	// ErrDuplicate means duplicate credential, i.e. non-unique login
+	ErrDuplicate = StoreError("duplicate value")
+	// ErrUnsupported means an operation is not supported
+	ErrUnsupported = StoreError("unsupported")
+	// ErrExpired means the secret has expired
+	ErrExpired = StoreError("expired")
+	// ErrPolicy means policy violation, e.g. password too weak.
+	ErrPolicy = StoreError("policy")
+	// ErrCredentials means credentials like email or captcha must be validated
+	ErrCredentials = StoreError("credentials")
+	// ErrNotFound means the objevy was not found
+	ErrNotFound = StoreError("not found")
+	// ErrPermissionDenied means the operation is not permitted
+	ErrPermissionDenied = StoreError("denied")
+)
+
+// Uid is a database-specific record id, suitable to be used as a primary key.
+type Uid uint64
+
+// ZeroUid is a constant representing uninitialized Uid.
+const ZeroUid Uid = 0
+
+// Lengths of various Uid representations
+const (
+	uidBase64Unpadded = 11
+	p2pBase64Unpadded = 22
+)
+
+// IsZero checks if Uid is uninitialized.
+func (uid Uid) IsZero() bool {
+	return uid == ZeroUid
+}
+
+// Compare returns 0 if uid is equal to u2, 1 if u2 is greater than uid, -1 if u2 is smaller.
+func (uid Uid) Compare(u2 Uid) int {
+	if uid < u2 {
+		return -1
+	} else if uid > u2 {
+		return 1
+	}
+	return 0
+}
+
+// MarshalBinary converts Uid to byte slice.
+func (uid *Uid) MarshalBinary() ([]byte, error) {
+	dst := make([]byte, 8)
+	binary.LittleEndian.PutUint64(dst, uint64(*uid))
+	return dst, nil
+}
+
+// UnmarshalBinary reads Uid from byte slice.
+func (uid *Uid) UnmarshalBinary(b []byte) error {
+	if len(b) < 8 {
+		return errors.New("Uid.UnmarshalBinary: invalid length")
+	}
+	*uid = Uid(binary.LittleEndian.Uint64(b))
+	return nil
+}
+
+// UnmarshalText reads Uid from string represented as byte slice.
+func (uid *Uid) UnmarshalText(src []byte) error {
+	if len(src) != uidBase64Unpadded {
+		return errors.New("Uid.UnmarshalText: invalid length")
+	}
+	dec := make([]byte, base64.URLEncoding.WithPadding(base64.NoPadding).DecodedLen(uidBase64Unpadded))
+	count, err := base64.URLEncoding.WithPadding(base64.NoPadding).Decode(dec, src)
+	if count < 8 {
+		if err != nil {
+			return errors.New("Uid.UnmarshalText: failed to decode " + err.Error())
+		}
+		return errors.New("Uid.UnmarshalText: failed to decode")
+	}
+	*uid = Uid(binary.LittleEndian.Uint64(dec))
+	return nil
+}
+
+// MarshalText converts Uid to string represented as byte slice.
+func (uid *Uid) MarshalText() ([]byte, error) {
+	if *uid == ZeroUid {
+		return []byte{}, nil
+	}
+	src := make([]byte, 8)
+	dst := make([]byte, base64.URLEncoding.WithPadding(base64.NoPadding).EncodedLen(8))
+	binary.LittleEndian.PutUint64(src, uint64(*uid))
+	base64.URLEncoding.WithPadding(base64.NoPadding).Encode(dst, src)
+	return dst, nil
+}
+
+// MarshalJSON converts Uid to double quoted ("ajjj") string.
+func (uid *Uid) MarshalJSON() ([]byte, error) {
+	dst, _ := uid.MarshalText()
+	return append(append([]byte{'"'}, dst...), '"'), nil
+}
+
+// UnmarshalJSON reads Uid from a double quoted string.
+func (uid *Uid) UnmarshalJSON(b []byte) error {
+	size := len(b)
+	if size != (uidBase64Unpadded + 2) {
+		return errors.New("Uid.UnmarshalJSON: invalid length")
+	} else if b[0] != '"' || b[size-1] != '"' {
+		return errors.New("Uid.UnmarshalJSON: unrecognized")
+	}
+	return uid.UnmarshalText(b[1 : size-1])
+}
+
+// String converts Uid to base64 string.
+func (uid Uid) String() string {
+	buf, _ := uid.MarshalText()
+	return string(buf)
+}
+
+// String32 converts Uid to lowercase base32 string (suitable for file names on Windows).
+func (uid Uid) String32() string {
+	data, _ := uid.MarshalBinary()
+	return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(data))
+}
+
+// ParseUid parses string NOT prefixed with anything
+func ParseUid(s string) Uid {
+	var uid Uid
+	uid.UnmarshalText([]byte(s))
+	return uid
+}
+
+// ParseUid32 parses base32-encoded string into Uid
+func ParseUid32(s string) Uid {
+	var uid Uid
+	if data, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(s); err == nil {
+		uid.UnmarshalBinary(data)
+	}
+	return uid
+}
+
+// UserId converts Uid to string prefixed with 'usr', like usrXXXXX
+func (uid Uid) UserId() string {
+	return uid.PrefixId("usr")
+}
+
+// FndName generates 'fnd' topic name for the given Uid.
+func (uid Uid) FndName() string {
+	return uid.PrefixId("fnd")
+}
+
+// PrefixId converts Uid to string prefixed with the given prefix.
+func (uid Uid) PrefixId(prefix string) string {
+	if uid.IsZero() {
+		return ""
+	}
+	return prefix + uid.String()
+}
+
+// ParseUserId parses user ID of the form "usrXXXXXX"
+func ParseUserId(s string) Uid {
+	var uid Uid
+	if strings.HasPrefix(s, "usr") {
+		(&uid).UnmarshalText([]byte(s)[3:])
+	}
+	return uid
+}
+
+// UidSlice is a slice of Uids sorted in ascending order.
+type UidSlice []Uid
+
+func (us UidSlice) find(uid Uid) (int, bool) {
+	l := len(us)
+	if l == 0 || us[0] > uid {
+		return 0, false
+	}
+	if uid > us[l-1] {
+		return l, false
+	}
+	idx := sort.Search(l, func(i int) bool {
+		return uid <= us[i]
+	})
+	return idx, idx < l && us[idx] == uid
+}
+
+// Add uid to UidSlice keeping it sorted.
+func (us *UidSlice) Add(uid Uid) bool {
+	idx, found := us.find(uid)
+	if found {
+		return false
+	}
+	// Inserting without creating a temporary slice.
+	*us = append(*us, ZeroUid)
+	copy((*us)[idx+1:], (*us)[idx:])
+	(*us)[idx] = uid
+	return true
+}
+
+// Rem removes uid from UidSlice.
+func (us *UidSlice) Rem(uid Uid) bool {
+	idx, found := us.find(uid)
+	if !found {
+		return false
+	}
+	if idx == len(*us)-1 {
+		*us = (*us)[:idx]
+	} else {
+		*us = append((*us)[:idx], (*us)[idx+1:]...)
+	}
+	return true
+}
+
+// Contains checks if the UidSlice contains the given uid
+func (us UidSlice) Contains(uid Uid) bool {
+	_, contains := us.find(uid)
+	return contains
+}
+
+// P2PName takes two Uids and generates a P2P topic name
+func (uid Uid) P2PName(u2 Uid) string {
+	if !uid.IsZero() && !u2.IsZero() {
+		b1, _ := uid.MarshalBinary()
+		b2, _ := u2.MarshalBinary()
+
+		if uid < u2 {
+			b1 = append(b1, b2...)
+		} else if uid > u2 {
+			b1 = append(b2, b1...)
+		} else {
+			// Explicitly disable P2P with self
+			return ""
+		}
+
+		return "p2p" + base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b1)
+	}
+
+	return ""
+}
+
+// ParseP2P extracts uids from the name of a p2p topic.
+func ParseP2P(p2p string) (uid1, uid2 Uid, err error) {
+	if strings.HasPrefix(p2p, "p2p") {
+		src := []byte(p2p)[3:]
+		if len(src) != p2pBase64Unpadded {
+			err = errors.New("ParseP2P: invalid length")
+			return
+		}
+		dec := make([]byte, base64.URLEncoding.WithPadding(base64.NoPadding).DecodedLen(p2pBase64Unpadded))
+		var count int
+		count, err = base64.URLEncoding.WithPadding(base64.NoPadding).Decode(dec, src)
+		if count < 16 {
+			if err != nil {
+				err = errors.New("ParseP2P: failed to decode " + err.Error())
+			} else {
+				err = errors.New("ParseP2P: invalid decoded length")
+			}
+			return
+		}
+		uid1 = Uid(binary.LittleEndian.Uint64(dec))
+		uid2 = Uid(binary.LittleEndian.Uint64(dec[8:]))
+	} else {
+		err = errors.New("ParseP2P: missing or invalid prefix")
+	}
+	return
+}
+
+// ObjHeader is the header shared by all stored objects.
+type ObjHeader struct {
+	Id        string // using string to get around rethinkdb's problems with unit64
+	id        Uid
+	CreatedAt time.Time
+	UpdatedAt time.Time
+	DeletedAt *time.Time `json:"DeletedAt,omitempty"`
+}
+
+// Uid assigns Uid header field.
+func (h *ObjHeader) Uid() Uid {
+	if h.id.IsZero() && h.Id != "" {
+		h.id.UnmarshalText([]byte(h.Id))
+	}
+	return h.id
+}
+
+// SetUid assigns given Uid to appropriate header fields.
+func (h *ObjHeader) SetUid(uid Uid) {
+	h.id = uid
+	h.Id = uid.String()
+}
+
+// TimeNow returns current wall time in UTC rounded to milliseconds.
+func TimeNow() time.Time {
+	return time.Now().UTC().Round(time.Millisecond)
+}
+
+// InitTimes initializes time.Time variables in the header to current time.
+func (h *ObjHeader) InitTimes() {
+	if h.CreatedAt.IsZero() {
+		h.CreatedAt = TimeNow()
+	}
+	h.UpdatedAt = h.CreatedAt
+	h.DeletedAt = nil
+}
+
+// MergeTimes intelligently copies time.Time variables from h2 to h.
+func (h *ObjHeader) MergeTimes(h2 *ObjHeader) {
+	// Set the creation time to the earliest value
+	if h.CreatedAt.IsZero() || (!h2.CreatedAt.IsZero() && h2.CreatedAt.Before(h.CreatedAt)) {
+		h.CreatedAt = h2.CreatedAt
+	}
+	// Set the update time to the latest value
+	if h.UpdatedAt.Before(h2.UpdatedAt) {
+		h.UpdatedAt = h2.UpdatedAt
+	}
+	// Set deleted time to the latest value
+	if h2.DeletedAt != nil && (h.DeletedAt == nil || h.DeletedAt.Before(*h2.DeletedAt)) {
+		h.DeletedAt = h2.DeletedAt
+	}
+}
+
+// IsDeleted returns true if the object is deleted.
+func (h *ObjHeader) IsDeleted() bool {
+	return h.DeletedAt != nil
+}
+
+// StringSlice is defined so Scanner and Valuer can be attached to it.
+type StringSlice []string
+
+// Scan implements sql.Scanner interface.
+func (ss *StringSlice) Scan(val interface{}) error {
+	return json.Unmarshal(val.([]byte), ss)
+}
+
+// Value implements sql/driver.Valuer interface.
+func (ss StringSlice) Value() (driver.Value, error) {
+	return json.Marshal(ss)
+}
+
+// User is a representation of a DB-stored user record.
+type User struct {
+	ObjHeader
+
+	State int
+
+	// Default access to user for P2P topics (used as default modeGiven)
+	Access DefaultAccess
+
+	// Values for 'me' topic:
+
+	// Last time when the user joined 'me' topic, by User Agent
+	LastSeen *time.Time
+	// User agent provided when accessing the topic last time
+	UserAgent string
+
+	Public interface{}
+
+	// Unique indexed tags (email, phone) for finding this user. Stored on the
+	// 'users' as well as indexed in 'tagunique'
+	Tags StringSlice
+
+	// Info on known devices, used for push notifications
+	Devices map[string]*DeviceDef
+}
+
+// AccessMode is a definition of access mode bits.
+type AccessMode uint
+
+// Various access mode constants
+const (
+	ModeJoin    AccessMode = 1 << iota // user can join, i.e. {sub} (J:1)
+	ModeRead                           // user can receive broadcasts ({data}, {info}) (R:2)
+	ModeWrite                          // user can Write, i.e. {pub} (W:4)
+	ModePres                           // user can receive presence updates (P:8)
+	ModeApprove                        // user can approve new members or evict existing members (A:0x10, 16)
+	ModeShare                          // user can invite new members (S:0x20, 32)
+	ModeDelete                         // user can hard-delete messages (D:0x40, 64)
+	ModeOwner                          // user is the owner (O:0x80, 128) - full access
+	ModeUnset                          // Non-zero value to indicate unknown or undefined mode (:0x100, 256),
+	// to make it different from ModeNone
+
+	ModeNone AccessMode = 0 // No access, requests to gain access are processed normally (N)
+
+	// Normal user's access to a topic ("JRWPS")
+	ModeCPublic AccessMode = ModeJoin | ModeRead | ModeWrite | ModePres | ModeShare
+	// User's subscription to 'me' and 'fnd' ("JPS")
+	ModeCSelf AccessMode = ModeJoin | ModePres | ModeShare
+	// Owner's subscription to a generic topic ("JRWPASDO")
+	ModeCFull AccessMode = ModeJoin | ModeRead | ModeWrite | ModePres | ModeApprove | ModeShare | ModeDelete | ModeOwner
+	// Default P2P access mode ("JRWPA")
+	ModeCP2P AccessMode = ModeJoin | ModeRead | ModeWrite | ModePres | ModeApprove
+	// Read-only access to topic ("JR", 0x3)
+	ModeCReadOnly = ModeJoin | ModeRead
+
+	// Admin: user who can modify access mode ("OA", hex: 0x90, dec: 144)
+	ModeCAdmin = ModeOwner | ModeApprove
+	// Sharer: flags which define user who can be notified of access mode changes ("OAS", dec: 176, hex: 0xB0)
+	ModeCSharer = ModeCAdmin | ModeShare
+
+	// Invalid mode to indicate an error
+	ModeInvalid AccessMode = 0x100000
+
+	// All possible valid bits (excluding ModeInvalid and ModeUnset)
+	ModeBitmask AccessMode = ModeJoin | ModeRead | ModeWrite | ModePres | ModeApprove | ModeShare | ModeDelete | ModeOwner
+)
+
+// MarshalText converts AccessMode to ASCII byte slice.
+func (m AccessMode) MarshalText() ([]byte, error) {
+	if m == ModeNone {
+		return []byte{'N'}, nil
+	}
+
+	if m == ModeInvalid {
+		return nil, errors.New("AccessMode invalid")
+	}
+
+	var res = []byte{}
+	var modes = []byte{'J', 'R', 'W', 'P', 'A', 'S', 'D', 'O'}
+	for i, chr := range modes {
+		if (m & (1 << uint(i))) != 0 {
+			res = append(res, chr)
+		}
+	}
+	return res, nil
+}
+
+// UnmarshalText parses access mode string as byte slice.
+// Does not change the mode if the string is empty or invalid.
+func (m *AccessMode) UnmarshalText(b []byte) error {
+	m0 := ModeUnset
+
+Loop:
+	for i := 0; i < len(b); i++ {
+		switch b[i] {
+		case 'J', 'j':
+			m0 |= ModeJoin
+		case 'R', 'r':
+			m0 |= ModeRead
+		case 'W', 'w':
+			m0 |= ModeWrite
+		case 'A', 'a':
+			m0 |= ModeApprove
+		case 'S', 's':
+			m0 |= ModeShare
+		case 'D', 'd':
+			m0 |= ModeDelete
+		case 'P', 'p':
+			m0 |= ModePres
+		case 'O', 'o':
+			m0 |= ModeOwner
+		case 'N', 'n':
+			m0 = ModeNone // N means explicitly no access, all bits cleared
+			break Loop
+		default:
+			return errors.New("AccessMode: invalid character '" + string(b[i]) + "'")
+		}
+	}
+
+	if m0 != ModeUnset {
+		*m = m0
+	}
+	return nil
+}
+
+// String returns string representation of AccessMode.
+func (m AccessMode) String() string {
+	res, err := m.MarshalText()
+	if err != nil {
+		return ""
+	}
+	return string(res)
+}
+
+// MarshalJSON converts AccessMode to a quoted string.
+func (m AccessMode) MarshalJSON() ([]byte, error) {
+	res, err := m.MarshalText()
+	if err != nil {
+		return nil, err
+	}
+
+	return append(append([]byte{'"'}, res...), '"'), nil
+}
+
+// UnmarshalJSON reads AccessMode from a quoted string.
+func (m *AccessMode) UnmarshalJSON(b []byte) error {
+	if b[0] != '"' || b[len(b)-1] != '"' {
+		return errors.New("syntax error")
+	}
+
+	return m.UnmarshalText(b[1 : len(b)-1])
+}
+
+// Scan is an implementation of sql.Scanner interface. It expects the
+// value to be a byte slice representation of an ASCII string.
+func (m *AccessMode) Scan(val interface{}) error {
+	if bb, ok := val.([]byte); ok {
+		return m.UnmarshalText(bb)
+	}
+	return errors.New("scan failed: data is not a byte slice")
+}
+
+// Value is an implementation of sql.driver.Valuer interface.
+func (m AccessMode) Value() (driver.Value, error) {
+	res, err := m.MarshalText()
+	if err != nil {
+		return "", err
+	}
+	return string(res), nil
+}
+
+// BetterThan checks if grant mode allows more permissions than requested in want mode.
+func (grant AccessMode) BetterThan(want AccessMode) bool {
+	return ModeBitmask&grant&^want != 0
+}
+
+// BetterEqual checks if grant mode allows all permissions requested in want mode.
+func (grant AccessMode) BetterEqual(want AccessMode) bool {
+	return ModeBitmask&grant&want == want
+}
+
+// Delta between two modes as a string old.Delta(new). JRPAS -> JRWS: "+W-PA"
+// Zero delta is an empty string ""
+func (o AccessMode) Delta(n AccessMode) string {
+	// Removed bits, bits present in 'old' but missing in 'new' -> '-'
+	o2n := ModeBitmask & o &^ n
+	var removed string
+	if o2n > 0 {
+		removed = o2n.String()
+		if removed != "" {
+			removed = "-" + removed
+		}
+	}
+
+	// Added bits, bits present in 'n' but missing in 'o' -> '+'
+	n2o := ModeBitmask & n &^ o
+	var added string
+	if n2o > 0 {
+		added = n2o.String()
+		if added != "" {
+			added = "+" + added
+		}
+	}
+	return added + removed
+}
+
+// IsJoiner checks if joiner flag J is set.
+func (m AccessMode) IsJoiner() bool {
+	return m&ModeJoin != 0
+}
+
+// IsOwner checks if owner bit O is set.
+func (m AccessMode) IsOwner() bool {
+	return m&ModeOwner != 0
+}
+
+// IsApprover checks if approver A bit is set.
+func (m AccessMode) IsApprover() bool {
+	return m&ModeApprove != 0
+}
+
+// IsAdmin check if owner O or approver A flag is set.
+func (m AccessMode) IsAdmin() bool {
+	return m.IsOwner() || m.IsApprover()
+}
+
+// IsSharer checks if approver A or sharer S or owner O flag is set.
+func (m AccessMode) IsSharer() bool {
+	return m.IsAdmin() || (m&ModeShare != 0)
+}
+
+// IsWriter checks if allowed to publish (writer flag W is set).
+func (m AccessMode) IsWriter() bool {
+	return m&ModeWrite != 0
+}
+
+// IsReader checks if reader flag R is set.
+func (m AccessMode) IsReader() bool {
+	return m&ModeRead != 0
+}
+
+// IsPresencer checks if user receives presence updates (P flag set).
+func (m AccessMode) IsPresencer() bool {
+	return m&ModePres != 0
+}
+
+// IsDeleter checks if user can hard-delete messages (D flag is set).
+func (m AccessMode) IsDeleter() bool {
+	return m&ModeDelete != 0
+}
+
+// IsZero checks if no flags are set.
+func (m AccessMode) IsZero() bool {
+	return m == ModeNone
+}
+
+// IsInvalid checks if mode is invalid.
+func (m AccessMode) IsInvalid() bool {
+	return m == ModeInvalid
+}
+
+// IsDefined checks if the mode is defined: not invalid and not unset.
+// ModeNone is considered to be defined.
+func (m AccessMode) IsDefined() bool {
+	return m != ModeInvalid && m != ModeUnset
+}
+
+// DefaultAccess is a per-topic default access modes
+type DefaultAccess struct {
+	Auth AccessMode
+	Anon AccessMode
+}
+
+// Scan is an implementation of Scanner interface so the value can be read from SQL DBs
+// It assumes the value is serialized and stored as JSON
+func (da *DefaultAccess) Scan(val interface{}) error {
+	return json.Unmarshal(val.([]byte), da)
+}
+
+// Value implements sql's driver.Valuer interface.
+func (da DefaultAccess) Value() (driver.Value, error) {
+	return json.Marshal(da)
+}
+
+// Credential hold data needed to validate and check validity of a credential like email or phone.
+type Credential struct {
+	ObjHeader
+	// Credential owner
+	User string
+	// Verification method (email, tel, captcha, etc)
+	Method string
+	// Credential value - `jdoe@example.com` or `+12345678901`
+	Value string
+	// Expected response
+	Resp string
+	// If credential was successfully confirmed
+	Done bool
+	// Retry count
+	Retries int
+}
+
+// Subscription to a topic
+type Subscription struct {
+	ObjHeader
+	// User who has relationship with the topic
+	User string
+	// Topic subscribed to
+	Topic string
+
+	// Values persisted through subscription soft-deletion
+
+	// ID of the latest Soft-delete operation
+	DelId int
+	// Last SeqId reported by user as received by at least one of his sessions
+	RecvSeqId int
+	// Last SeqID reported read by the user
+	ReadSeqId int
+
+	// Access mode requested by this user
+	ModeWant AccessMode
+	// Access mode granted to this user
+	ModeGiven AccessMode
+	// User's private data associated with the subscription to topic
+	Private interface{}
+
+	// Deserialized ephemeral values
+
+	// Deserialized public value from topic or user (depends on context)
+	// In case of P2P topics this is the Public value of the other user.
+	public interface{}
+	// deserialized SeqID from user or topic
+	seqId int
+	// Deserialized TouchedAt from topic
+	touchedAt *time.Time
+	// timestamp when the user was last online
+	lastSeen time.Time
+	// user agent string of the last online access
+	userAgent string
+
+	// P2P only. ID of the other user
+	with string
+	// P2P only. Default access: this is the mode given by the other user to this user
+	modeDefault *DefaultAccess
+}
+
+// SetPublic assigns to public, otherwise not accessible from outside the package.
+func (s *Subscription) SetPublic(pub interface{}) {
+	s.public = pub
+}
+
+// GetPublic reads value of public.
+func (s *Subscription) GetPublic() interface{} {
+	return s.public
+}
+
+// SetWith sets other user for P2P subscriptions.
+func (s *Subscription) SetWith(with string) {
+	s.with = with
+}
+
+// GetWith returns the other user for P2P subscriptions.
+func (s *Subscription) GetWith() string {
+	return s.with
+}
+
+// GetTouchedAt returns touchedAt.
+func (s *Subscription) GetTouchedAt() *time.Time {
+	return s.touchedAt
+}
+
+// SetTouchedAt sets the value of touchedAt.
+func (s *Subscription) SetTouchedAt(touchedAt *time.Time) {
+	if s.touchedAt == nil || touchedAt.After(*s.touchedAt) {
+		s.touchedAt = touchedAt
+	}
+
+	if s.touchedAt.Before(s.UpdatedAt) {
+		s.touchedAt = &s.UpdatedAt
+	}
+}
+
+// GetSeqId returns seqId.
+func (s *Subscription) GetSeqId() int {
+	return s.seqId
+}
+
+// SetSeqId sets seqId field.
+func (s *Subscription) SetSeqId(id int) {
+	s.seqId = id
+}
+
+// GetLastSeen returns lastSeen.
+func (s *Subscription) GetLastSeen() time.Time {
+	return s.lastSeen
+}
+
+// GetUserAgent returns userAgent.
+func (s *Subscription) GetUserAgent() string {
+	return s.userAgent
+}
+
+// SetLastSeenAndUA updates lastSeen time and userAgent.
+func (s *Subscription) SetLastSeenAndUA(when *time.Time, ua string) {
+	if when != nil {
+		s.lastSeen = *when
+	}
+	s.userAgent = ua
+}
+
+// SetDefaultAccess updates default access values.
+func (s *Subscription) SetDefaultAccess(auth, anon AccessMode) {
+	s.modeDefault = &DefaultAccess{auth, anon}
+}
+
+// GetDefaultAccess returns default access.
+func (s *Subscription) GetDefaultAccess() *DefaultAccess {
+	return s.modeDefault
+}
+
+// Contact is a result of a search for connections
+type Contact struct {
+	Id       string
+	MatchOn  []string
+	Access   DefaultAccess
+	LastSeen time.Time
+	Public   interface{}
+}
+
+type perUserData struct {
+	private interface{}
+	want    AccessMode
+	given   AccessMode
+}
+
+// Topic stored in database. Topic's name is Id
+type Topic struct {
+	ObjHeader
+
+	// Timestamp when the last message has passed through the topic
+	TouchedAt *time.Time
+
+	// Use bearer token or use ACL
+	UseBt bool
+
+	// Topic owner. Could be zero
+	Owner string
+
+	// Default access to topic
+	Access DefaultAccess
+
+	// Server-issued sequential ID
+	SeqId int
+	// If messages were deleted, sequential id of the last operation to delete them
+	DelId int
+
+	Public interface{}
+
+	// Indexed tags for finding this topic.
+	Tags StringSlice
+
+	// Deserialized ephemeral params
+	perUser map[Uid]*perUserData // deserialized from Subscription
+}
+
+// GiveAccess updates access mode for the given user.
+func (t *Topic) GiveAccess(uid Uid, want, given AccessMode) {
+	if t.perUser == nil {
+		t.perUser = make(map[Uid]*perUserData, 1)
+	}
+
+	pud := t.perUser[uid]
+	if pud == nil {
+		pud = &perUserData{}
+	}
+
+	pud.want = want
+	pud.given = given
+
+	t.perUser[uid] = pud
+	if want&given&ModeOwner != 0 && t.Owner == "" {
+		t.Owner = uid.String()
+	}
+}
+
+// SetPrivate updates private value for the given user.
+func (t *Topic) SetPrivate(uid Uid, private interface{}) {
+	if t.perUser == nil {
+		t.perUser = make(map[Uid]*perUserData, 1)
+	}
+	pud := t.perUser[uid]
+	if pud == nil {
+		pud = &perUserData{}
+	}
+	pud.private = private
+	t.perUser[uid] = pud
+}
+
+// GetPrivate returns given user's private value.
+func (t *Topic) GetPrivate(uid Uid) (private interface{}) {
+	if t.perUser == nil {
+		return
+	}
+	pud := t.perUser[uid]
+	if pud == nil {
+		return
+	}
+	private = pud.private
+	return
+}
+
+// GetAccess returns given user's access mode.
+func (t *Topic) GetAccess(uid Uid) (mode AccessMode) {
+	if t.perUser == nil {
+		return
+	}
+	pud := t.perUser[uid]
+	if pud == nil {
+		return
+	}
+	mode = pud.given & pud.want
+	return
+}
+
+// SoftDelete is a single DB record of soft-deletetion.
+type SoftDelete struct {
+	User  string
+	DelId int
+}
+
+// MessageHeaders is needed to attach Scan() to.
+type MessageHeaders map[string]interface{}
+
+// Scan implements sql.Scanner interface.
+func (mh *MessageHeaders) Scan(val interface{}) error {
+	return json.Unmarshal(val.([]byte), mh)
+}
+
+// Value implements sql's driver.Valuer interface.
+func (mh MessageHeaders) Value() (driver.Value, error) {
+	return json.Marshal(mh)
+}
+
+// Message is a stored {data} message
+type Message struct {
+	ObjHeader
+	// ID of the hard-delete operation
+	DelId int `json:"DelId,omitempty"`
+	// List of users who have marked this message as soft-deleted
+	DeletedFor []SoftDelete `json:"DeletedFor,omitempty"`
+	SeqId      int
+	Topic      string
+	// Sender's user ID as string (without 'usr' prefix), could be empty.
+	From    string
+	Head    MessageHeaders `json:"Head,omitempty"`
+	Content interface{}
+}
+
+// Range is a range of message SeqIDs. Low end is inclusive (closed), high end is exclusive (open): [Low, Hi).
+// If the range contains just one ID, Hi is set to 0
+type Range struct {
+	Low int
+	Hi  int `json:"Hi,omitempty"`
+}
+
+// RangeSorter is a helper type required by 'sort' package.
+type RangeSorter []Range
+
+// Len is the length of the range.
+func (rs RangeSorter) Len() int {
+	return len(rs)
+}
+
+// Swap swaps two items in a slice.
+func (rs RangeSorter) Swap(i, j int) {
+	rs[i], rs[j] = rs[j], rs[i]
+}
+
+// Less is a comparator. Sort by Low ascending, then sort by Hi descending
+func (rs RangeSorter) Less(i, j int) bool {
+	if rs[i].Low < rs[j].Low {
+		return true
+	}
+	if rs[i].Low == rs[j].Low {
+		return rs[i].Hi >= rs[j].Hi
+	}
+	return false
+}
+
+// Normalize ranges - remove overlaps: [1..4],[2..4],[5..7] -> [1..7].
+// The ranges are expected to be sorted.
+// Ranges are inclusive-inclusive, i.e. [1..3] -> 1, 2, 3.
+func (rs RangeSorter) Normalize() RangeSorter {
+	ll := rs.Len()
+	if ll > 1 {
+		prev := 0
+		for i := 1; i < ll; i++ {
+			if rs[prev].Low == rs[i].Low {
+				// Earlier range is guaranteed to be wider or equal to the later range,
+				// collapse two ranges into one (by doing nothing)
+				continue
+			}
+			// Check for full or partial overlap
+			if rs[prev].Hi > 0 && rs[prev].Hi+1 >= rs[i].Low {
+				// Partial overlap
+				if rs[prev].Hi < rs[i].Hi {
+					rs[prev].Hi = rs[i].Hi
+				}
+				// Otherwise the next range is fully within the previous range, consume it by doing nothing.
+				continue
+			}
+			// No overlap
+			prev++
+		}
+		rs = rs[:prev+1]
+	}
+
+	return rs
+}
+
+// DelMessage is a log entry of a deleted message range.
+type DelMessage struct {
+	ObjHeader
+	Topic       string
+	DeletedFor  string
+	DelId       int
+	SeqIdRanges []Range
+}
+
+// QueryOpt is options of a query, [since, before] - both ends inclusive (closed)
+type QueryOpt struct {
+	// Subscription query
+	User            Uid
+	Topic           string
+	IfModifiedSince *time.Time
+	// ID-based query parameters: Messages
+	Since  int
+	Before int
+	// Common parameter
+	Limit int
+}
+
+// TopicCat is an enum of topic categories.
+type TopicCat int
+
+const (
+	// TopicCatMe is a value denoting 'me' topic.
+	TopicCatMe TopicCat = iota
+	// TopicCatFnd is a value denoting 'fnd' topic.
+	TopicCatFnd
+	// TopicCatP2P is a a value denoting 'p2p topic.
+	TopicCatP2P
+	// TopicCatGrp is a a value denoting group topic.
+	TopicCatGrp
+)
+
+// GetTopicCat given topic name returns topic category.
+func GetTopicCat(name string) TopicCat {
+	switch name[:3] {
+	case "usr":
+		return TopicCatMe
+	case "p2p":
+		return TopicCatP2P
+	case "grp":
+		return TopicCatGrp
+	case "fnd":
+		return TopicCatFnd
+	default:
+		panic("invalid topic type for name '" + name + "'")
+	}
+}
+
+// DeviceDef is the data provided by connected device. Used primarily for
+// push notifications.
+type DeviceDef struct {
+	// Device registration ID
+	DeviceId string
+	// Device platform (iOS, Android, Web)
+	Platform string
+	// Last logged in
+	LastSeen time.Time
+	// Device language, ISO code
+	Lang string
+}
+
+// Media handling constants
+const (
+	// UploadStarted indicates that the upload has started but not finished yet.
+	UploadStarted = iota
+	// UploadCompleted indicates that the upload has completed successfully.
+	UploadCompleted
+	// UploadFailed indicates that the upload has failed.
+	UploadFailed
+)
+
+// FileDef is a stored record of a file upload
+type FileDef struct {
+	ObjHeader
+	// Status of upload
+	Status int
+	// User who created the file
+	User string
+	// Type of the file.
+	MimeType string
+	// Size of the file in bytes.
+	Size int64
+	// Internal file location, i.e. path on disk or an S3 blob address.
+	Location string
+}

+ 92 - 0
tinodeService/store/types/uidgen.go

@@ -0,0 +1,92 @@
+package types
+
+import (
+	"encoding/base64"
+	"encoding/binary"
+
+	sf "github.com/tinode/snowflake"
+	"golang.org/x/crypto/xtea"
+)
+
+// UidGenerator holds snowflake and encryption paramenets.
+// RethinkDB generates UUIDs as primary keys. Using snowflake-generated uint64 instead.
+type UidGenerator struct {
+	seq    *sf.SnowFlake
+	cipher *xtea.Cipher
+}
+
+// Init initialises the Uid generator
+func (ug *UidGenerator) Init(workerID uint, key []byte) error {
+	var err error
+
+	if ug.seq == nil {
+		ug.seq, err = sf.NewSnowFlake(uint32(workerID))
+	}
+	if ug.cipher == nil {
+		ug.cipher, err = xtea.NewCipher(key)
+	}
+
+	return err
+}
+
+// Get generates a unique weakly-encryped random-looking ID.
+// The Uid is a unit64 with the highest bit possibly set which makes it
+// incompatible with go's pre-1.9 sql package.
+func (ug *UidGenerator) Get() Uid {
+	buf, err := getIDBuffer(ug)
+	if err != nil {
+		return ZeroUid
+	}
+	return Uid(binary.LittleEndian.Uint64(buf))
+}
+
+// GetStr generates the same unique ID as Get then returns it as
+// base64-encoded string. Slightly more efficient than calling Get()
+// then base64-encoding the result.
+func (ug *UidGenerator) GetStr() string {
+	buf, err := getIDBuffer(ug)
+	if err != nil {
+		return ""
+	}
+	return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(buf)
+}
+
+// getIdBuffer returns a byte array holding the Uid bytes
+func getIDBuffer(ug *UidGenerator) ([]byte, error) {
+	var id uint64
+	var err error
+	if id, err = ug.seq.Next(); err != nil {
+		return nil, err
+	}
+
+	var src = make([]byte, 8)
+	var dst = make([]byte, 8)
+	binary.LittleEndian.PutUint64(src, id)
+	ug.cipher.Encrypt(dst, src)
+
+	return dst, nil
+}
+
+// DecodeUid takes an encrypted Uid and decrypts it into a non-negative int64.
+// This is needed for go/sql compatibility where uint64 with high bit
+// set is unsupported and possibly for other uses such as MySQL's recommendation
+// for sequential primary keys.
+func (ug *UidGenerator) DecodeUid(uid Uid) int64 {
+	var src = make([]byte, 8)
+	var dst = make([]byte, 8)
+	binary.LittleEndian.PutUint64(src, uint64(uid))
+	ug.cipher.Decrypt(dst, src)
+	return int64(binary.LittleEndian.Uint64(dst))
+}
+
+// EncodeInt64 takes a positive int64 and encrypts it into a Uid.
+// This is needed for go/sql compatibility where uint64 with high bit
+// set is unsupported  and possibly for other uses such as MySQL's recommendation
+// for sequential primary keys.
+func (ug *UidGenerator) EncodeInt64(val int64) Uid {
+	var src = make([]byte, 8)
+	var dst = make([]byte, 8)
+	binary.LittleEndian.PutUint64(src, uint64(val))
+	ug.cipher.Encrypt(dst, src)
+	return Uid(binary.LittleEndian.Uint64(dst))
+}