summaryrefslogtreecommitdiffstats
path: root/store
diff options
context:
space:
mode:
author=Corey Hulen <corey@hulen.com>2015-06-14 23:53:32 -0800
committer=Corey Hulen <corey@hulen.com>2015-06-14 23:53:32 -0800
commit56e74239d6b34df8f30ef046f0b0ff4ff0866a71 (patch)
tree044da29848cf0f5c8607eac34de69065171669cf /store
downloadchat-56e74239d6b34df8f30ef046f0b0ff4ff0866a71.tar.gz
chat-56e74239d6b34df8f30ef046f0b0ff4ff0866a71.tar.bz2
chat-56e74239d6b34df8f30ef046f0b0ff4ff0866a71.zip
first commit
Diffstat (limited to 'store')
-rw-r--r--store/redis.go75
-rw-r--r--store/redis_test.go59
-rw-r--r--store/sql_audit_store.go88
-rw-r--r--store/sql_audit_store_test.go40
-rw-r--r--store/sql_channel_store.go554
-rw-r--r--store/sql_channel_store_test.go493
-rw-r--r--store/sql_post_store.go410
-rw-r--r--store/sql_post_store_test.go481
-rw-r--r--store/sql_session_store.go176
-rw-r--r--store/sql_session_store_test.go134
-rw-r--r--store/sql_store.go372
-rw-r--r--store/sql_store_test.go83
-rw-r--r--store/sql_team_store.go195
-rw-r--r--store/sql_team_store_test.go155
-rw-r--r--store/sql_user_store.go367
-rw-r--r--store/sql_user_store_test.go276
-rw-r--r--store/store.go94
17 files changed, 4052 insertions, 0 deletions
diff --git a/store/redis.go b/store/redis.go
new file mode 100644
index 000000000..262040d43
--- /dev/null
+++ b/store/redis.go
@@ -0,0 +1,75 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ l4g "code.google.com/p/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "gopkg.in/redis.v2"
+ "strings"
+ "time"
+)
+
+var client *redis.Client
+
+func RedisClient() *redis.Client {
+
+ if client == nil {
+
+ addr := utils.Cfg.RedisSettings.DataSource
+
+ client = redis.NewTCPClient(&redis.Options{
+ Addr: addr,
+ Password: "",
+ DB: 0,
+ PoolSize: utils.Cfg.RedisSettings.MaxOpenConns,
+ })
+
+ l4g.Info("Pinging redis at '%v'", addr)
+ pong, err := client.Ping().Result()
+
+ if err != nil {
+ l4g.Critical("Failed to open redis connection to '%v' err:%v", addr, err)
+ time.Sleep(time.Second)
+ panic("Failed to open redis connection " + err.Error())
+ }
+
+ if pong != "PONG" {
+ l4g.Critical("Failed to ping redis connection to '%v' err:%v", addr, err)
+ time.Sleep(time.Second)
+ panic("Failed to open ping connection " + err.Error())
+ }
+ }
+
+ return client
+}
+
+func RedisClose() {
+ l4g.Info("Closing redis")
+
+ if client != nil {
+ client.Close()
+ client = nil
+ }
+}
+
+func PublishAndForget(message *model.Message) {
+
+ go func() {
+ c := RedisClient()
+ result := c.Publish(message.TeamId, message.ToJson())
+ if result.Err() != nil {
+ l4g.Error("Failed to publish message err=%v, payload=%v", result.Err(), message.ToJson())
+ }
+ }()
+}
+
+func GetMessageFromPayload(m interface{}) *model.Message {
+ if msg, found := m.(*redis.Message); found {
+ return model.MessageFromJson(strings.NewReader(msg.Payload))
+ } else {
+ return nil
+ }
+}
diff --git a/store/redis_test.go b/store/redis_test.go
new file mode 100644
index 000000000..11bd9ca6a
--- /dev/null
+++ b/store/redis_test.go
@@ -0,0 +1,59 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "fmt"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "testing"
+)
+
+func TestRedis(t *testing.T) {
+ utils.LoadConfig("config.json")
+
+ c := RedisClient()
+
+ if c == nil {
+ t.Fatal("should have a valid redis connection")
+ }
+
+ pubsub := c.PubSub()
+ defer pubsub.Close()
+
+ m := model.NewMessage(model.NewId(), model.NewId(), model.NewId(), model.ACTION_TYPING)
+ m.Add("RootId", model.NewId())
+
+ err := pubsub.Subscribe(m.TeamId)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // should be the subscribe success message
+ // lets gobble that up
+ if _, err := pubsub.Receive(); err != nil {
+ t.Fatal(err)
+ }
+
+ PublishAndForget(m)
+
+ fmt.Println("here1")
+
+ if msg, err := pubsub.Receive(); err != nil {
+ t.Fatal(err)
+ } else {
+
+ rmsg := GetMessageFromPayload(msg)
+
+ if m.TeamId != rmsg.TeamId {
+ t.Fatal("Ids do not match")
+ }
+
+ if m.Props["RootId"] != rmsg.Props["RootId"] {
+ t.Fatal("Ids do not match")
+ }
+ }
+
+ RedisClose()
+}
diff --git a/store/sql_audit_store.go b/store/sql_audit_store.go
new file mode 100644
index 000000000..dd9312007
--- /dev/null
+++ b/store/sql_audit_store.go
@@ -0,0 +1,88 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+type SqlAuditStore struct {
+ *SqlStore
+}
+
+func NewSqlAuditStore(sqlStore *SqlStore) AuditStore {
+ s := &SqlAuditStore{sqlStore}
+
+ for _, db := range sqlStore.GetAllConns() {
+ table := db.AddTableWithName(model.Audit{}, "Audits").SetKeys(false, "Id")
+ table.ColMap("Id").SetMaxSize(26)
+ table.ColMap("UserId").SetMaxSize(26)
+ table.ColMap("Action").SetMaxSize(64)
+ table.ColMap("ExtraInfo").SetMaxSize(128)
+ table.ColMap("IpAddress").SetMaxSize(64)
+ table.ColMap("SessionId").SetMaxSize(26)
+ }
+
+ return s
+}
+
+func (s SqlAuditStore) UpgradeSchemaIfNeeded() {
+}
+
+func (s SqlAuditStore) CreateIndexesIfNotExists() {
+ s.CreateIndexIfNotExists("idx_user_id", "Audits", "UserId")
+}
+
+func (s SqlAuditStore) Save(audit *model.Audit) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ audit.Id = model.NewId()
+ audit.CreateAt = model.GetMillis()
+
+ if err := s.GetMaster().Insert(audit); err != nil {
+ result.Err = model.NewAppError("SqlAuditStore.Save",
+ "We encounted an error saving the audit", "user_id="+
+ audit.UserId+" action="+audit.Action)
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlAuditStore) Get(user_id string, limit int) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if limit > 1000 {
+ limit = 1000
+ result.Err = model.NewAppError("SqlAuditStore.Get", "Limit exceeded for paging", "user_id="+user_id)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ var audits model.Audits
+ if _, err := s.GetReplica().Select(&audits, "SELECT * FROM Audits WHERE UserId = ? ORDER BY CreateAt DESC LIMIT ?",
+ user_id, limit); err != nil {
+ result.Err = model.NewAppError("SqlAuditStore.Get", "We encounted an error finding the audits", "user_id="+user_id)
+ } else {
+ result.Data = audits
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_audit_store_test.go b/store/sql_audit_store_test.go
new file mode 100644
index 000000000..3e6f22730
--- /dev/null
+++ b/store/sql_audit_store_test.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "testing"
+)
+
+func TestSqlAuditStore(t *testing.T) {
+ Setup()
+
+ audit := &model.Audit{UserId: model.NewId(), IpAddress: "ipaddress", Action: "Action"}
+ <-store.Audit().Save(audit)
+ <-store.Audit().Save(audit)
+ <-store.Audit().Save(audit)
+ audit.ExtraInfo = "extra"
+ <-store.Audit().Save(audit)
+
+ c := store.Audit().Get(audit.UserId, 100)
+ result := <-c
+ audits := result.Data.(model.Audits)
+
+ if len(audits) != 4 {
+ t.Fatal("Failed to save and retrieve 4 audit logs")
+ }
+
+ if audits[0].ExtraInfo != "extra" {
+ t.Fatal("Failed to save property for extra info")
+ }
+
+ c = store.Audit().Get("missing", 100)
+ result = <-c
+ audits = result.Data.(model.Audits)
+
+ if len(audits) != 0 {
+ t.Fatal("Should have returned empty because user_id is missing")
+ }
+}
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
new file mode 100644
index 000000000..592657c1c
--- /dev/null
+++ b/store/sql_channel_store.go
@@ -0,0 +1,554 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "strings"
+)
+
+type SqlChannelStore struct {
+ *SqlStore
+}
+
+func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
+ s := &SqlChannelStore{sqlStore}
+
+ for _, db := range sqlStore.GetAllConns() {
+ table := db.AddTableWithName(model.Channel{}, "Channels").SetKeys(false, "Id")
+ table.ColMap("Id").SetMaxSize(26)
+ table.ColMap("TeamId").SetMaxSize(26)
+ table.ColMap("Type").SetMaxSize(1)
+ table.ColMap("DisplayName").SetMaxSize(64)
+ table.ColMap("Name").SetMaxSize(64)
+ table.SetUniqueTogether("Name", "TeamId")
+ table.ColMap("Description").SetMaxSize(1024)
+
+ tablem := db.AddTableWithName(model.ChannelMember{}, "ChannelMembers").SetKeys(false, "ChannelId", "UserId")
+ tablem.ColMap("ChannelId").SetMaxSize(26)
+ tablem.ColMap("UserId").SetMaxSize(26)
+ tablem.ColMap("Roles").SetMaxSize(64)
+ tablem.ColMap("NotifyLevel").SetMaxSize(20)
+ }
+
+ return s
+}
+
+func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
+}
+
+func (s SqlChannelStore) CreateIndexesIfNotExists() {
+ s.CreateIndexIfNotExists("idx_team_id", "Channels", "TeamId")
+}
+
+func (s SqlChannelStore) Save(channel *model.Channel) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if len(channel.Id) > 0 {
+ result.Err = model.NewAppError("SqlChannelStore.Save",
+ "Must call update for exisiting channel", "id="+channel.Id)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ channel.PreSave()
+ if result.Err = channel.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if count, err := s.GetMaster().SelectInt("SELECT COUNT(0) FROM Channels WHERE TeamId = ? AND DeleteAt = 0 AND (Type ='O' || Type ='P')", channel.TeamId); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.Save", "Failed to get current channel count", "teamId="+channel.TeamId+", "+err.Error())
+ storeChannel <- result
+ close(storeChannel)
+ return
+ } else if count > 150 {
+ result.Err = model.NewAppError("SqlChannelStore.Save", "You've reached the limit of the number of allowed channels.", "teamId="+channel.TeamId)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if err := s.GetMaster().Insert(channel); err != nil {
+ if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Name'") {
+ result.Err = model.NewAppError("SqlChannelStore.Save", "A channel with that name already exists", "id="+channel.Id+", "+err.Error())
+ } else {
+ result.Err = model.NewAppError("SqlChannelStore.Save", "We couldn't save the channel", "id="+channel.Id+", "+err.Error())
+ }
+ } else {
+ result.Data = channel
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) Update(channel *model.Channel) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ channel.PreUpdate()
+
+ if result.Err = channel.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if count, err := s.GetMaster().Update(channel); err != nil {
+ if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Name'") {
+ result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that name already exists", "id="+channel.Id+", "+err.Error())
+ } else {
+ result.Err = model.NewAppError("SqlChannelStore.Update", "We encounted an error updating the channel", "id="+channel.Id+", "+err.Error())
+ }
+ } else if count != 1 {
+ result.Err = model.NewAppError("SqlChannelStore.Update", "We couldn't update the channel", "id="+channel.Id)
+ } else {
+ result.Data = channel
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) Get(id string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if obj, err := s.GetReplica().Get(model.Channel{}, id); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.Get", "We encounted an error finding the channel", "id="+id+", "+err.Error())
+ } else if obj == nil {
+ result.Err = model.NewAppError("SqlChannelStore.Get", "We couldn't find the existing channel", "id="+id)
+ } else {
+ result.Data = obj.(*model.Channel)
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) Delete(channelId string, time int64) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("Update Channels SET DeleteAt = ?, UpdateAt = ? WHERE Id = ?", time, time, channelId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.Delete", "We couldn't delete the channel", "id="+channelId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+type channelWithMember struct {
+ model.Channel
+ model.ChannelMember
+}
+
+func (s SqlChannelStore) GetChannels(teamId string, userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var data []channelWithMember
+ _, err := s.GetReplica().Select(&data, "SELECT * FROM Channels, ChannelMembers WHERE Id = ChannelId AND TeamId = ? AND UserId = ? AND DeleteAt = 0 ORDER BY DisplayName", teamId, userId)
+
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.GetChannels", "We couldn't get the channels", "teamId="+teamId+", userId="+userId+", err="+err.Error())
+ } else {
+ channels := &model.ChannelList{make([]*model.Channel, len(data)), make(map[string]*model.ChannelMember)}
+ for i := range data {
+ v := data[i]
+ channels.Channels[i] = &v.Channel
+ channels.Members[v.Channel.Id] = &v.ChannelMember
+ }
+
+ if len(channels.Channels) == 0 {
+ result.Err = model.NewAppError("SqlChannelStore.GetChannels", "No channels were found", "teamId="+teamId+", userId="+userId)
+ } else {
+ result.Data = channels
+ }
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var data []*model.Channel
+ _, err := s.GetReplica().Select(&data,
+ `SELECT
+ *
+ FROM
+ Channels
+ WHERE
+ TeamId = ?
+ AND Type IN ("O")
+ AND DeleteAt = 0
+ AND Id NOT IN (SELECT
+ Channels.Id
+ FROM
+ Channels,
+ ChannelMembers
+ WHERE
+ Id = ChannelId
+ AND TeamId = ?
+ AND UserId = ?
+ AND DeleteAt = 0)
+ ORDER BY DisplayName`,
+ teamId, teamId, userId)
+
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.GetMoreChannels", "We couldn't get the channels", "teamId="+teamId+", userId="+userId+", err="+err.Error())
+ } else {
+ result.Data = &model.ChannelList{data, make(map[string]*model.ChannelMember)}
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) GetByName(teamId string, name string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ channel := model.Channel{}
+
+ if err := s.GetReplica().SelectOne(&channel, "SELECT * FROM Channels WHERE TeamId=? AND Name=? AND DeleteAt = 0", teamId, name); err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.GetByName", "We couldn't find the existing channel", "teamId="+teamId+", "+"name="+name+", "+err.Error())
+ } else {
+ result.Data = &channel
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if result.Err = member.IsValid(); result.Err != nil {
+ storeChannel <- result
+ return
+ }
+
+ if err := s.GetMaster().Insert(member); err != nil {
+ if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'ChannelId'") {
+ result.Err = model.NewAppError("SqlChannelStore.SaveMember", "A channel member with that id already exists", "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error())
+ } else {
+ result.Err = model.NewAppError("SqlChannelStore.SaveMember", "We couldn't save the channel member", "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error())
+ }
+ } else {
+ result.Data = member
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) GetMembers(channelId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var members []model.ChannelMember
+ _, err := s.GetReplica().Select(&members, "SELECT * FROM ChannelMembers WHERE ChannelId = ?", channelId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.GetMembers", "We couldn't get the channel members", "channel_id="+channelId+err.Error())
+ } else {
+ result.Data = members
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) GetMember(channelId string, userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var member model.ChannelMember
+ err := s.GetReplica().SelectOne(&member, "SELECT * FROM ChannelMembers WHERE ChannelId = ? AND UserId = ?", channelId, userId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.GetMember", "We couldn't get the channel member", "channel_id="+channelId+"user_id="+userId+","+err.Error())
+ } else {
+ result.Data = member
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) GetExtraMembers(channelId string, limit int) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var members []model.ExtraMember
+ _, err := s.GetReplica().Select(&members, "SELECT Id, FullName, Email, ChannelMembers.Roles, Username FROM ChannelMembers, Users WHERE ChannelMembers.UserId = Users.Id AND ChannelId = ? LIMIT ?", channelId, limit)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.GetExtraMembers", "We couldn't get the extra info for channel members", "channel_id="+channelId+", "+err.Error())
+ } else {
+ for i, _ := range members {
+ members[i].Sanitize(utils.SanitizeOptions)
+ }
+ result.Data = members
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) RemoveMember(channelId string, userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("DELETE FROM ChannelMembers WHERE ChannelId = ? AND UserId = ?", channelId, userId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.RemoveMember", "We couldn't remove the channel member", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ count, err := s.GetReplica().SelectInt(
+ `SELECT
+ COUNT(0)
+ FROM
+ Channels,
+ ChannelMembers
+ WHERE
+ Channels.Id = ChannelMembers.ChannelId
+ AND Channels.TeamId = ?
+ AND Channels.DeleteAt = 0
+ AND ChannelMembers.ChannelId = ?
+ AND ChannelMembers.UserId = ?`,
+ teamId, channelId, userId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.CheckPermissionsTo", "We couldn't check the permissions", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
+ } else {
+ result.Data = count
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ channelId, err := s.GetReplica().SelectStr(
+ `SELECT
+ Channels.Id
+ FROM
+ Channels,
+ ChannelMembers
+ WHERE
+ Channels.Id = ChannelMembers.ChannelId
+ AND Channels.TeamId = ?
+ AND Channels.Name = ?
+ AND Channels.DeleteAt = 0
+ AND ChannelMembers.UserId = ?`,
+ teamId, channelName, userId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.CheckPermissionsToByName", "We couldn't check the permissions", "channel_id="+channelName+", user_id="+userId+", "+err.Error())
+ } else {
+ result.Data = channelId
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) CheckOpenChannelPermissions(teamId string, channelId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ count, err := s.GetReplica().SelectInt(
+ `SELECT
+ COUNT(0)
+ FROM
+ Channels
+ WHERE
+ Channels.Id = ?
+ AND Channels.TeamId = ?
+ AND Channels.Type = ?`,
+ channelId, teamId, model.CHANNEL_OPEN)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.CheckOpenChannelPermissions", "We couldn't check the permissions", "channel_id="+channelId+", "+err.Error())
+ } else {
+ result.Data = count
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) UpdateLastViewedAt(channelId string, userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec(
+ `UPDATE
+ ChannelMembers, Channels
+ SET
+ ChannelMembers.MentionCount = 0,
+ ChannelMembers.MsgCount = Channels.TotalMsgCount,
+ ChannelMembers.LastViewedAt = Channels.LastPostAt
+ WHERE
+ Channels.Id = ChannelMembers.ChannelId
+ AND UserId = ?
+ AND ChannelId = ?`,
+ userId, channelId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.UpdateLastViewedAt", "We couldn't update the last viewed at time", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec(
+ `UPDATE
+ ChannelMembers
+ SET
+ MentionCount = MentionCount + 1
+ WHERE
+ UserId = ?
+ AND ChannelId = ?`,
+ userId, channelId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.IncrementMentionCount", "We couldn't increment the mention count", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlChannelStore) UpdateNotifyLevel(channelId, userId, notifyLevel string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec(
+ `UPDATE
+ ChannelMembers
+ SET
+ NotifyLevel = ?
+ WHERE
+ UserId = ?
+ AND ChannelId = ?`,
+ notifyLevel, userId, channelId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.UpdateNotifyLevel", "We couldn't update the notify level", "channel_id="+channelId+", user_id="+userId+", "+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go
new file mode 100644
index 000000000..ce4ff75f0
--- /dev/null
+++ b/store/sql_channel_store_test.go
@@ -0,0 +1,493 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "testing"
+)
+
+func TestChannelStoreSave(t *testing.T) {
+ Setup()
+
+ teamId := model.NewId()
+
+ o1 := model.Channel{}
+ o1.TeamId = teamId
+ o1.DisplayName = "Name"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+
+ if err := (<-store.Channel().Save(&o1)).Err; err != nil {
+ t.Fatal("couldn't save item", err)
+ }
+
+ if err := (<-store.Channel().Save(&o1)).Err; err == nil {
+ t.Fatal("shouldn't be able to update from save")
+ }
+
+ o1.Id = ""
+ if err := (<-store.Channel().Save(&o1)).Err; err == nil {
+ t.Fatal("should be unique name")
+ }
+
+ for i := 0; i < 150; i++ {
+ o1.Id = ""
+ o1.Name = "a" + model.NewId() + "b"
+ if err := (<-store.Channel().Save(&o1)).Err; err != nil {
+ t.Fatal("couldn't save item", err)
+ }
+ }
+
+ o1.Id = ""
+ o1.Name = "a" + model.NewId() + "b"
+ if err := (<-store.Channel().Save(&o1)).Err; err == nil {
+ t.Fatal("should be the limit")
+ }
+}
+
+func TestChannelStoreUpdate(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Name"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o1)
+
+ if err := (<-store.Channel().Update(&o1)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ o1.Id = "missing"
+ if err := (<-store.Channel().Update(&o1)).Err; err == nil {
+ t.Fatal("Update should have failed because of missing key")
+ }
+
+ o1.Id = model.NewId()
+ if err := (<-store.Channel().Update(&o1)).Err; err == nil {
+ t.Fatal("Update should have faile because id change")
+ }
+}
+
+func TestChannelStoreGet(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Name"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o1)
+
+ if r1 := <-store.Channel().Get(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Channel).ToJson() != o1.ToJson() {
+ t.Fatal("invalid returned channel")
+ }
+ }
+
+ if err := (<-store.Channel().Get("")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestChannelStoreDelete(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Channel1"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o1)
+
+ o2 := model.Channel{}
+ o2.TeamId = o1.TeamId
+ o2.DisplayName = "Channel2"
+ o2.Name = "a" + model.NewId() + "b"
+ o2.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o2)
+
+ o3 := model.Channel{}
+ o3.TeamId = o1.TeamId
+ o3.DisplayName = "Channel3"
+ o3.Name = "a" + model.NewId() + "b"
+ o3.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o3)
+
+ o4 := model.Channel{}
+ o4.TeamId = o1.TeamId
+ o4.DisplayName = "Channel4"
+ o4.Name = "a" + model.NewId() + "b"
+ o4.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o4)
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = o1.Id
+ m1.UserId = model.NewId()
+ m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m1)
+
+ m2 := model.ChannelMember{}
+ m2.ChannelId = o2.Id
+ m2.UserId = m1.UserId
+ m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m2)
+
+ if r := <-store.Channel().Delete(o1.Id, model.GetMillis()); r.Err != nil {
+ t.Fatal(r.Err)
+ }
+
+ if r := <-store.Channel().Get(o1.Id); r.Data.(*model.Channel).DeleteAt == 0 {
+ t.Fatal("should have been deleted")
+ }
+
+ if r := <-store.Channel().Delete(o3.Id, model.GetMillis()); r.Err != nil {
+ t.Fatal(r.Err)
+ }
+
+ cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId)
+ list := cresult.Data.(*model.ChannelList)
+
+ if len(list.Channels) != 1 {
+ t.Fatal("invalid number of channels")
+ }
+
+ cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId)
+ list = cresult.Data.(*model.ChannelList)
+
+ if len(list.Channels) != 1 {
+ t.Fatal("invalid number of channels")
+ }
+}
+
+func TestChannelStoreGetByName(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Name"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o1)
+
+ if r1 := <-store.Channel().GetByName(o1.TeamId, o1.Name); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Channel).ToJson() != o1.ToJson() {
+ t.Fatal("invalid returned channel")
+ }
+ }
+
+ if err := (<-store.Channel().GetByName(o1.TeamId, "")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestChannelMemberStore(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ u1.FullName = model.NewId()
+ <-store.User().Save(&u1)
+
+ u2 := model.User{}
+ u2.TeamId = model.NewId()
+ u2.Email = model.NewId()
+ u2.FullName = model.NewId()
+ <-store.User().Save(&u2)
+
+ o1 := model.ChannelMember{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = u1.Id
+ o1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&o1)
+
+ o2 := model.ChannelMember{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = u2.Id
+ o2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&o2)
+
+ members := (<-store.Channel().GetMembers(o1.ChannelId)).Data.([]model.ChannelMember)
+ if len(members) != 2 {
+ t.Fatal("should have saved 2 members")
+ }
+
+ <-store.Channel().RemoveMember(o2.ChannelId, o2.UserId)
+
+ members = (<-store.Channel().GetMembers(o1.ChannelId)).Data.([]model.ChannelMember)
+ if len(members) != 1 {
+ t.Fatal("should have removed 1 member")
+ }
+
+ member := (<-store.Channel().GetMember(o1.ChannelId, o1.UserId)).Data.(model.ChannelMember)
+ if member.ChannelId != o1.ChannelId {
+ t.Fatal("should have go member")
+ }
+
+ extraMembers := (<-store.Channel().GetExtraMembers(o1.ChannelId, 20)).Data.([]model.ExtraMember)
+ if len(extraMembers) != 1 {
+ t.Fatal("should have 1 extra members")
+ }
+
+ if err := (<-store.Channel().SaveMember(&o1)).Err; err == nil {
+ t.Fatal("Should have been a duplicate")
+ }
+}
+
+func TestChannelStorePermissionsTo(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Channel1"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o1)
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = o1.Id
+ m1.UserId = model.NewId()
+ m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m1)
+
+ count := (<-store.Channel().CheckPermissionsTo(o1.TeamId, o1.Id, m1.UserId)).Data.(int64)
+ if count != 1 {
+ t.Fatal("should have permissions")
+ }
+
+ count = (<-store.Channel().CheckPermissionsTo("junk", o1.Id, m1.UserId)).Data.(int64)
+ if count != 0 {
+ t.Fatal("shouldn't have permissions")
+ }
+
+ count = (<-store.Channel().CheckPermissionsTo(o1.TeamId, "junk", m1.UserId)).Data.(int64)
+ if count != 0 {
+ t.Fatal("shouldn't have permissions")
+ }
+
+ count = (<-store.Channel().CheckPermissionsTo(o1.TeamId, o1.Id, "junk")).Data.(int64)
+ if count != 0 {
+ t.Fatal("shouldn't have permissions")
+ }
+
+ channelId := (<-store.Channel().CheckPermissionsToByName(o1.TeamId, o1.Name, m1.UserId)).Data.(string)
+ if channelId != o1.Id {
+ t.Fatal("should have permissions")
+ }
+
+ channelId = (<-store.Channel().CheckPermissionsToByName(o1.TeamId, "missing", m1.UserId)).Data.(string)
+ if channelId != "" {
+ t.Fatal("should not have permissions")
+ }
+}
+
+func TestChannelStoreOpenChannelPermissionsTo(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Channel1"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o1)
+
+ count := (<-store.Channel().CheckOpenChannelPermissions(o1.TeamId, o1.Id)).Data.(int64)
+ if count != 1 {
+ t.Fatal("should have permissions")
+ }
+
+ count = (<-store.Channel().CheckOpenChannelPermissions("junk", o1.Id)).Data.(int64)
+ if count != 0 {
+ t.Fatal("shouldn't have permissions")
+ }
+
+ count = (<-store.Channel().CheckOpenChannelPermissions(o1.TeamId, "junk")).Data.(int64)
+ if count != 0 {
+ t.Fatal("shouldn't have permissions")
+ }
+}
+
+func TestChannelStoreGetChannels(t *testing.T) {
+ Setup()
+
+ o2 := model.Channel{}
+ o2.TeamId = model.NewId()
+ o2.DisplayName = "Channel2"
+ o2.Name = "a" + model.NewId() + "b"
+ o2.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o2)
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Channel1"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o1)
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = o1.Id
+ m1.UserId = model.NewId()
+ m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m1)
+
+ m2 := model.ChannelMember{}
+ m2.ChannelId = o1.Id
+ m2.UserId = model.NewId()
+ m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m2)
+
+ m3 := model.ChannelMember{}
+ m3.ChannelId = o2.Id
+ m3.UserId = model.NewId()
+ m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m3)
+
+ cresult := <-store.Channel().GetChannels(o1.TeamId, m1.UserId)
+ list := cresult.Data.(*model.ChannelList)
+
+ if list.Channels[0].Id != o1.Id {
+ t.Fatal("missing channel")
+ }
+}
+
+func TestChannelStoreGetMoreChannels(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Channel1"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o1)
+
+ o2 := model.Channel{}
+ o2.TeamId = model.NewId()
+ o2.DisplayName = "Channel2"
+ o2.Name = "a" + model.NewId() + "b"
+ o2.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o2)
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = o1.Id
+ m1.UserId = model.NewId()
+ m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m1)
+
+ m2 := model.ChannelMember{}
+ m2.ChannelId = o1.Id
+ m2.UserId = model.NewId()
+ m2.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m2)
+
+ m3 := model.ChannelMember{}
+ m3.ChannelId = o2.Id
+ m3.UserId = model.NewId()
+ m3.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m3)
+
+ o3 := model.Channel{}
+ o3.TeamId = o1.TeamId
+ o3.DisplayName = "ChannelA"
+ o3.Name = "a" + model.NewId() + "b"
+ o3.Type = model.CHANNEL_OPEN
+ <-store.Channel().Save(&o3)
+
+ o4 := model.Channel{}
+ o4.TeamId = o1.TeamId
+ o4.DisplayName = "ChannelB"
+ o4.Name = "a" + model.NewId() + "b"
+ o4.Type = model.CHANNEL_PRIVATE
+ <-store.Channel().Save(&o4)
+
+ o5 := model.Channel{}
+ o5.TeamId = o1.TeamId
+ o5.DisplayName = "ChannelC"
+ o5.Name = "a" + model.NewId() + "b"
+ o5.Type = model.CHANNEL_PRIVATE
+ <-store.Channel().Save(&o5)
+
+ cresult := <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId)
+ list := cresult.Data.(*model.ChannelList)
+
+ if len(list.Channels) != 1 {
+ t.Fatal("wrong list")
+ }
+
+ if list.Channels[0].Name != o3.Name {
+ t.Fatal("missing channel")
+ }
+}
+
+func TestChannelStoreUpdateLastViewedAt(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Channel1"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ o1.TotalMsgCount = 25
+ <-store.Channel().Save(&o1)
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = o1.Id
+ m1.UserId = model.NewId()
+ m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m1)
+
+ err := (<-store.Channel().UpdateLastViewedAt(m1.ChannelId, m1.UserId)).Err
+ if err != nil {
+ t.Fatal("failed to update")
+ }
+
+ err = (<-store.Channel().UpdateLastViewedAt(m1.ChannelId, "missing id")).Err
+ if err != nil {
+ t.Fatal("failed to update")
+ }
+}
+
+func TestChannelStoreIncrementMentionCount(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Channel1"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ o1.TotalMsgCount = 25
+ <-store.Channel().Save(&o1)
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = o1.Id
+ m1.UserId = model.NewId()
+ m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m1)
+
+ err := (<-store.Channel().IncrementMentionCount(m1.ChannelId, m1.UserId)).Err
+ if err != nil {
+ t.Fatal("failed to update")
+ }
+
+ err = (<-store.Channel().IncrementMentionCount(m1.ChannelId, "missing id")).Err
+ if err != nil {
+ t.Fatal("failed to update")
+ }
+
+ err = (<-store.Channel().IncrementMentionCount("missing id", m1.UserId)).Err
+ if err != nil {
+ t.Fatal("failed to update")
+ }
+
+ err = (<-store.Channel().IncrementMentionCount("missing id", "missing id")).Err
+ if err != nil {
+ t.Fatal("failed to update")
+ }
+}
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
new file mode 100644
index 000000000..0ceebc02f
--- /dev/null
+++ b/store/sql_post_store.go
@@ -0,0 +1,410 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "fmt"
+ "github.com/mattermost/platform/model"
+ "strconv"
+ "strings"
+)
+
+type SqlPostStore struct {
+ *SqlStore
+}
+
+func NewSqlPostStore(sqlStore *SqlStore) PostStore {
+ s := &SqlPostStore{sqlStore}
+
+ for _, db := range sqlStore.GetAllConns() {
+ table := db.AddTableWithName(model.Post{}, "Posts").SetKeys(false, "Id")
+ table.ColMap("Id").SetMaxSize(26)
+ table.ColMap("UserId").SetMaxSize(26)
+ table.ColMap("ChannelId").SetMaxSize(26)
+ table.ColMap("RootId").SetMaxSize(26)
+ table.ColMap("ParentId").SetMaxSize(26)
+ table.ColMap("Message").SetMaxSize(4000)
+ table.ColMap("Type").SetMaxSize(26)
+ table.ColMap("Hashtags").SetMaxSize(1000)
+ table.ColMap("Props").SetMaxSize(4000)
+ table.ColMap("Filenames").SetMaxSize(4000)
+ }
+
+ return s
+}
+
+func (s SqlPostStore) UpgradeSchemaIfNeeded() {
+}
+
+func (s SqlPostStore) CreateIndexesIfNotExists() {
+ s.CreateIndexIfNotExists("idx_update_at", "Posts", "UpdateAt")
+ s.CreateIndexIfNotExists("idx_create_at", "Posts", "CreateAt")
+ s.CreateIndexIfNotExists("idx_channel_id", "Posts", "ChannelId")
+ s.CreateIndexIfNotExists("idx_root_id", "Posts", "RootId")
+
+ s.CreateFullTextIndexIfNotExists("idx_message_txt", "Posts", "Message")
+ s.CreateFullTextIndexIfNotExists("idx_hashtags_txt", "Posts", "Hashtags")
+}
+
+func (s SqlPostStore) Save(post *model.Post) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if len(post.Id) > 0 {
+ result.Err = model.NewAppError("SqlPostStore.Save",
+ "You cannot update an existing Post", "id="+post.Id)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ post.PreSave()
+ if result.Err = post.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if err := s.GetMaster().Insert(post); err != nil {
+ result.Err = model.NewAppError("SqlPostStore.Save", "We couldn't save the Post", "id="+post.Id+", "+err.Error())
+ } else {
+ time := model.GetMillis()
+ s.GetMaster().Exec("UPDATE Channels SET LastPostAt = ?, TotalMsgCount = TotalMsgCount + 1 WHERE Id = ?", time, post.ChannelId)
+
+ if len(post.RootId) > 0 {
+ s.GetMaster().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ?", time, post.RootId)
+ }
+
+ result.Data = post
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) Update(oldPost *model.Post, newMessage string, newHashtags string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ editPost := *oldPost
+ editPost.Message = newMessage
+ editPost.UpdateAt = model.GetMillis()
+ editPost.Hashtags = newHashtags
+
+ oldPost.DeleteAt = editPost.UpdateAt
+ oldPost.UpdateAt = editPost.UpdateAt
+ oldPost.OriginalId = oldPost.Id
+ oldPost.Id = model.NewId()
+
+ if result.Err = editPost.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if _, err := s.GetMaster().Update(&editPost); err != nil {
+ result.Err = model.NewAppError("SqlPostStore.Update", "We couldn't update the Post", "id="+editPost.Id+", "+err.Error())
+ } else {
+ time := model.GetMillis()
+ s.GetMaster().Exec("UPDATE Channels SET LastPostAt = ? WHERE Id = ?", time, editPost.ChannelId)
+
+ if len(editPost.RootId) > 0 {
+ s.GetMaster().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ?", time, editPost.RootId)
+ }
+
+ // mark the old post as deleted
+ s.GetMaster().Insert(oldPost)
+
+ result.Data = &editPost
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) Get(id string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+ pl := &model.PostList{}
+
+ var post model.Post
+ err := s.GetReplica().SelectOne(&post, "SELECT * FROM Posts WHERE Id = ? AND DeleteAt = 0", id)
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.GetPost", "We couldn't get the post", "id="+id+err.Error())
+ }
+
+ if post.ImgCount > 0 {
+ post.Filenames = []string{}
+ for i := 0; int64(i) < post.ImgCount; i++ {
+ fileUrl := "/api/v1/files/get_image/" + post.ChannelId + "/" + post.Id + "/" + strconv.Itoa(i+1) + ".png"
+ post.Filenames = append(post.Filenames, fileUrl)
+ }
+ }
+
+ pl.AddPost(&post)
+ pl.AddOrder(id)
+
+ rootId := post.RootId
+
+ if rootId == "" {
+ rootId = post.Id
+ }
+
+ var posts []*model.Post
+ _, err = s.GetReplica().Select(&posts, "SELECT * FROM Posts WHERE (Id = ? OR RootId = ?) AND DeleteAt = 0", rootId, rootId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.GetPost", "We couldn't get the post", "root_id="+rootId+err.Error())
+ } else {
+ for _, p := range posts {
+ pl.AddPost(p)
+ }
+ }
+
+ result.Data = pl
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+type etagPosts struct {
+ Id string
+ UpdateAt int64
+}
+
+func (s SqlPostStore) GetEtag(channelId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var et etagPosts
+ err := s.GetReplica().SelectOne(&et, "SELECT Id, UpdateAt FROM Posts WHERE ChannelId = ? ORDER BY UpdateAt DESC LIMIT 1", channelId)
+ if err != nil {
+ result.Data = fmt.Sprintf("%v.0.%v", model.ETAG_ROOT_VERSION, model.GetMillis())
+ } else {
+ result.Data = fmt.Sprintf("%v.%v.%v", model.ETAG_ROOT_VERSION, et.Id, et.UpdateAt)
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) Delete(postId string, time int64) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("Update Posts SET DeleteAt = ?, UpdateAt = ? WHERE Id = ? OR ParentId = ? OR RootId = ?", time, time, postId, postId, postId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.Delete", "We couldn't delete the post", "id="+postId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) GetPosts(channelId string, offset int, limit int) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if limit > 1000 {
+ result.Err = model.NewAppError("SqlPostStore.GetLinearPosts", "Limit exceeded for paging", "channelId="+channelId)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ rpc := s.getRootPosts(channelId, offset, limit)
+ cpc := s.getParentsPosts(channelId, offset, limit)
+
+ if rpr := <-rpc; rpr.Err != nil {
+ result.Err = rpr.Err
+ } else if cpr := <-cpc; cpr.Err != nil {
+ result.Err = cpr.Err
+ } else {
+ posts := rpr.Data.([]*model.Post)
+ parents := cpr.Data.([]*model.Post)
+
+ list := &model.PostList{Order: make([]string, 0, len(posts))}
+
+ for _, p := range posts {
+ if p.ImgCount > 0 {
+ p.Filenames = []string{}
+ for i := 0; int64(i) < p.ImgCount; i++ {
+ fileUrl := "/api/v1/files/get_image/" + p.ChannelId + "/" + p.Id + "/" + strconv.Itoa(i+1) + ".png"
+ p.Filenames = append(p.Filenames, fileUrl)
+ }
+ }
+ list.AddPost(p)
+ list.AddOrder(p.Id)
+ }
+
+ for _, p := range parents {
+ if p.ImgCount > 0 {
+ p.Filenames = []string{}
+ for i := 0; int64(i) < p.ImgCount; i++ {
+ fileUrl := "/api/v1/files/get_image/" + p.ChannelId + "/" + p.Id + "/" + strconv.Itoa(i+1) + ".png"
+ p.Filenames = append(p.Filenames, fileUrl)
+ }
+ }
+ list.AddPost(p)
+ }
+
+ list.MakeNonNil()
+
+ result.Data = list
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) getRootPosts(channelId string, offset int, limit int) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var posts []*model.Post
+ _, err := s.GetReplica().Select(&posts, "SELECT * FROM Posts WHERE ChannelId = ? AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT ?,?", channelId, offset, limit)
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.GetLinearPosts", "We couldn't get the posts for the channel", "channelId="+channelId+err.Error())
+ } else {
+ result.Data = posts
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) getParentsPosts(channelId string, offset int, limit int) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var posts []*model.Post
+ _, err := s.GetReplica().Select(&posts,
+ `SELECT
+ q2.*
+ FROM
+ Posts q2
+ INNER JOIN
+ (SELECT DISTINCT
+ q3.RootId
+ FROM
+ (SELECT
+ RootId
+ FROM
+ Posts
+ WHERE
+ ChannelId = ?
+ AND DeleteAt = 0
+ ORDER BY CreateAt DESC
+ LIMIT ?, ?) q3) q1 ON q1.RootId = q2.RootId
+ WHERE
+ ChannelId = ?
+ AND DeleteAt = 0
+ ORDER BY CreateAt`,
+ channelId, offset, limit, channelId)
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.GetLinearPosts", "We couldn't get the parent post for the channel", "channelId="+channelId+err.Error())
+ } else {
+ result.Data = posts
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+ searchType := "Message"
+ if isHashtagSearch {
+ searchType = "Hashtags"
+ }
+
+ // @ has a speical meaning in INNODB FULLTEXT indexes and
+ // is reserved for calc'ing distances so you
+ // cannot escape it so we replace it.
+ terms = strings.Replace(terms, "@", " ", -1)
+
+ searchQuery := fmt.Sprintf(`SELECT
+ *
+ FROM
+ Posts
+ WHERE
+ DeleteAt = 0
+ AND ChannelId IN (SELECT
+ Id
+ FROM
+ Channels,
+ ChannelMembers
+ WHERE
+ Id = ChannelId AND TeamId = ?
+ AND UserId = ?
+ AND DeleteAt = 0)
+ AND MATCH (%s) AGAINST (? IN BOOLEAN MODE)
+ ORDER BY CreateAt DESC
+ LIMIT 100`, searchType)
+
+ var posts []*model.Post
+ _, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms)
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error())
+ } else {
+
+ list := &model.PostList{Order: make([]string, 0, len(posts))}
+
+ for _, p := range posts {
+ list.AddPost(p)
+ list.AddOrder(p.Id)
+ }
+
+ list.MakeNonNil()
+
+ result.Data = list
+ }
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
new file mode 100644
index 000000000..13dd6e5ed
--- /dev/null
+++ b/store/sql_post_store_test.go
@@ -0,0 +1,481 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestPostStoreSave(t *testing.T) {
+ Setup()
+
+ o1 := model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+
+ if err := (<-store.Post().Save(&o1)).Err; err != nil {
+ t.Fatal("couldn't save item", err)
+ }
+
+ if err := (<-store.Post().Save(&o1)).Err; err == nil {
+ t.Fatal("shouldn't be able to update from save")
+ }
+}
+
+func TestPostStoreGet(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+
+ etag1 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string)
+ if strings.Index(etag1, model.ETAG_ROOT_VERSION+".0.") != 0 {
+ t.Fatal("Invalid Etag")
+ }
+
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ etag2 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string)
+ if strings.Index(etag2, model.ETAG_ROOT_VERSION+"."+o1.Id) != 0 {
+ t.Fatal("Invalid Etag")
+ }
+
+ if r1 := <-store.Post().Get(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.PostList).Posts[o1.Id].CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned post")
+ }
+ }
+
+ if err := (<-store.Post().Get("123")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestPostStoreUpdate(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "AAAAAAAAAAA"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ o2 := &model.Post{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = model.NewId()
+ o2.Message = "a" + model.NewId() + "CCCCCCCCC"
+ o2.ParentId = o1.Id
+ o2.RootId = o1.Id
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+
+ o3 := &model.Post{}
+ o3.ChannelId = o1.ChannelId
+ o3.UserId = model.NewId()
+ o3.Message = "a" + model.NewId() + "QQQQQQQQQQ"
+ o3 = (<-store.Post().Save(o3)).Data.(*model.Post)
+
+ ro1 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o1.Id]
+ ro2 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o2.Id]
+ ro6 := (<-store.Post().Get(o3.Id)).Data.(*model.PostList).Posts[o3.Id]
+
+ if ro1.Message != o1.Message {
+ t.Fatal("Failed to save/get")
+ }
+
+ msg := o1.Message + "BBBBBBBBBB"
+ if result := <-store.Post().Update(ro1, msg, ""); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ msg2 := o2.Message + "DDDDDDD"
+ if result := <-store.Post().Update(ro2, msg2, ""); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ msg3 := o3.Message + "WWWWWWW"
+ if result := <-store.Post().Update(ro6, msg3, "#hashtag"); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ ro3 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o1.Id]
+
+ if ro3.Message != msg {
+ t.Fatal("Failed to update/get")
+ }
+
+ ro4 := (<-store.Post().Get(o1.Id)).Data.(*model.PostList).Posts[o2.Id]
+
+ if ro4.Message != msg2 {
+ t.Fatal("Failed to update/get")
+ }
+
+ ro5 := (<-store.Post().Get(o3.Id)).Data.(*model.PostList).Posts[o3.Id]
+
+ if ro5.Message != msg3 && ro5.Hashtags != "#hashtag" {
+ t.Fatal("Failed to update/get")
+ }
+
+}
+
+func TestPostStoreDelete(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+
+ etag1 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string)
+ if strings.Index(etag1, model.ETAG_ROOT_VERSION+".0.") != 0 {
+ t.Fatal("Invalid Etag")
+ }
+
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ if r1 := <-store.Post().Get(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.PostList).Posts[o1.Id].CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned post")
+ }
+ }
+
+ if r2 := <-store.Post().Delete(o1.Id, model.GetMillis()); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Post().Get(o1.Id)); r3.Err == nil {
+ t.Log(r3.Data)
+ t.Fatal("Missing id should have failed")
+ }
+
+ etag2 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string)
+ if strings.Index(etag2, model.ETAG_ROOT_VERSION+"."+o1.Id) != 0 {
+ t.Fatal("Invalid Etag")
+ }
+}
+
+func TestPostStoreDelete1Level(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ o2 := &model.Post{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = model.NewId()
+ o2.Message = "a" + model.NewId() + "b"
+ o2.ParentId = o1.Id
+ o2.RootId = o1.Id
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+
+ if r2 := <-store.Post().Delete(o1.Id, model.GetMillis()); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Post().Get(o1.Id)); r3.Err == nil {
+ t.Fatal("Deleted id should have failed")
+ }
+
+ if r4 := (<-store.Post().Get(o2.Id)); r4.Err == nil {
+ t.Fatal("Deleted id should have failed")
+ }
+}
+
+func TestPostStoreDelete2Level(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ o2 := &model.Post{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = model.NewId()
+ o2.Message = "a" + model.NewId() + "b"
+ o2.ParentId = o1.Id
+ o2.RootId = o1.Id
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+
+ o3 := &model.Post{}
+ o3.ChannelId = o1.ChannelId
+ o3.UserId = model.NewId()
+ o3.Message = "a" + model.NewId() + "b"
+ o3.ParentId = o2.Id
+ o3.RootId = o1.Id
+ o3 = (<-store.Post().Save(o3)).Data.(*model.Post)
+
+ o4 := &model.Post{}
+ o4.ChannelId = model.NewId()
+ o4.UserId = model.NewId()
+ o4.Message = "a" + model.NewId() + "b"
+ o4 = (<-store.Post().Save(o4)).Data.(*model.Post)
+
+ if r2 := <-store.Post().Delete(o1.Id, model.GetMillis()); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Post().Get(o1.Id)); r3.Err == nil {
+ t.Fatal("Deleted id should have failed")
+ }
+
+ if r4 := (<-store.Post().Get(o2.Id)); r4.Err == nil {
+ t.Fatal("Deleted id should have failed")
+ }
+
+ if r5 := (<-store.Post().Get(o3.Id)); r5.Err == nil {
+ t.Fatal("Deleted id should have failed")
+ }
+
+ if r6 := <-store.Post().Get(o4.Id); r6.Err != nil {
+ t.Fatal(r6.Err)
+ }
+}
+
+func TestPostStoreGetWithChildren(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ o2 := &model.Post{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = model.NewId()
+ o2.Message = "a" + model.NewId() + "b"
+ o2.ParentId = o1.Id
+ o2.RootId = o1.Id
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+
+ o3 := &model.Post{}
+ o3.ChannelId = o1.ChannelId
+ o3.UserId = model.NewId()
+ o3.Message = "a" + model.NewId() + "b"
+ o3.ParentId = o2.Id
+ o3.RootId = o1.Id
+ o3 = (<-store.Post().Save(o3)).Data.(*model.Post)
+
+ if r1 := <-store.Post().Get(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ pl := r1.Data.(*model.PostList)
+ if len(pl.Posts) != 3 {
+ t.Fatal("invalid returned post")
+ }
+ }
+
+ <-store.Post().Delete(o3.Id, model.GetMillis())
+
+ if r2 := <-store.Post().Get(o1.Id); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ pl := r2.Data.(*model.PostList)
+ if len(pl.Posts) != 2 {
+ t.Fatal("invalid returned post")
+ }
+ }
+
+ <-store.Post().Delete(o2.Id, model.GetMillis())
+
+ if r3 := <-store.Post().Get(o1.Id); r3.Err != nil {
+ t.Fatal(r3.Err)
+ } else {
+ pl := r3.Data.(*model.PostList)
+ if len(pl.Posts) != 1 {
+ t.Fatal("invalid returned post")
+ }
+ }
+}
+
+func TestPostStoreGetPostsWtihDetails(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o2 := &model.Post{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = model.NewId()
+ o2.Message = "a" + model.NewId() + "b"
+ o2.ParentId = o1.Id
+ o2.RootId = o1.Id
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o2a := &model.Post{}
+ o2a.ChannelId = o1.ChannelId
+ o2a.UserId = model.NewId()
+ o2a.Message = "a" + model.NewId() + "b"
+ o2a.ParentId = o1.Id
+ o2a.RootId = o1.Id
+ o2a = (<-store.Post().Save(o2a)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o3 := &model.Post{}
+ o3.ChannelId = o1.ChannelId
+ o3.UserId = model.NewId()
+ o3.Message = "a" + model.NewId() + "b"
+ o3.ParentId = o1.Id
+ o3.RootId = o1.Id
+ o3 = (<-store.Post().Save(o3)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o4 := &model.Post{}
+ o4.ChannelId = o1.ChannelId
+ o4.UserId = model.NewId()
+ o4.Message = "a" + model.NewId() + "b"
+ o4 = (<-store.Post().Save(o4)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o5 := &model.Post{}
+ o5.ChannelId = o1.ChannelId
+ o5.UserId = model.NewId()
+ o5.Message = "a" + model.NewId() + "b"
+ o5.ParentId = o4.Id
+ o5.RootId = o4.Id
+ o5 = (<-store.Post().Save(o5)).Data.(*model.Post)
+
+ r1 := (<-store.Post().GetPosts(o1.ChannelId, 0, 4)).Data.(*model.PostList)
+
+ if r1.Order[0] != o5.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r1.Order[1] != o4.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r1.Order[2] != o3.Id {
+ t.Fatal("invalid order")
+ }
+
+ if r1.Order[3] != o2a.Id {
+ t.Fatal("invalid order")
+ }
+
+ if len(r1.Posts) != 6 {
+ t.Fatal("wrong size")
+ }
+
+ if r1.Posts[o1.Id].Message != o1.Message {
+ t.Fatal("Missing parent")
+ }
+}
+
+func TestPostStoreSearch(t *testing.T) {
+ Setup()
+
+ teamId := model.NewId()
+ userId := model.NewId()
+
+ c1 := &model.Channel{}
+ c1.TeamId = teamId
+ c1.DisplayName = "Channel1"
+ c1.Name = "a" + model.NewId() + "b"
+ c1.Type = model.CHANNEL_OPEN
+ c1 = (<-store.Channel().Save(c1)).Data.(*model.Channel)
+
+ m1 := model.ChannelMember{}
+ m1.ChannelId = c1.Id
+ m1.UserId = userId
+ m1.NotifyLevel = model.CHANNEL_NOTIFY_ALL
+ <-store.Channel().SaveMember(&m1)
+
+ c2 := &model.Channel{}
+ c2.TeamId = teamId
+ c2.DisplayName = "Channel1"
+ c2.Name = "a" + model.NewId() + "b"
+ c2.Type = model.CHANNEL_OPEN
+ c2 = (<-store.Channel().Save(c2)).Data.(*model.Channel)
+
+ o1 := &model.Post{}
+ o1.ChannelId = c1.Id
+ o1.UserId = model.NewId()
+ o1.Message = "corey mattermost new york"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ o2 := &model.Post{}
+ o2.ChannelId = c1.Id
+ o2.UserId = model.NewId()
+ o2.Message = "New Jersey is where John is from"
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+
+ o3 := &model.Post{}
+ o3.ChannelId = c2.Id
+ o3.UserId = model.NewId()
+ o3.Message = "New Jersey is where John is from corey new york"
+ o3 = (<-store.Post().Save(o3)).Data.(*model.Post)
+
+ o4 := &model.Post{}
+ o4.ChannelId = c1.Id
+ o4.UserId = model.NewId()
+ o4.Hashtags = "#hashtag"
+ o4.Message = "message"
+ o4 = (<-store.Post().Save(o4)).Data.(*model.Post)
+
+ o5 := &model.Post{}
+ o5.ChannelId = c1.Id
+ o5.UserId = model.NewId()
+ o5.Hashtags = "#secret #howdy"
+ o5 = (<-store.Post().Save(o5)).Data.(*model.Post)
+
+ r1 := (<-store.Post().Search(teamId, userId, "corey", false)).Data.(*model.PostList)
+ if len(r1.Order) != 1 && r1.Order[0] != o1.Id {
+ t.Fatal("returned wrong serach result")
+ }
+
+ r2 := (<-store.Post().Search(teamId, userId, "new york", false)).Data.(*model.PostList)
+ if len(r2.Order) != 2 && r2.Order[0] != o2.Id {
+ t.Fatal("returned wrong serach result")
+ }
+
+ r3 := (<-store.Post().Search(teamId, userId, "new", false)).Data.(*model.PostList)
+ if len(r3.Order) != 2 && r3.Order[0] != o1.Id {
+ t.Fatal("returned wrong serach result")
+ }
+
+ r4 := (<-store.Post().Search(teamId, userId, "john", false)).Data.(*model.PostList)
+ if len(r4.Order) != 1 && r4.Order[0] != o2.Id {
+ t.Fatal("returned wrong serach result")
+ }
+
+ r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList)
+ if len(r5.Order) != 1 && r5.Order[0] != o1.Id {
+ t.Fatal("returned wrong serach result")
+ }
+
+ r6 := (<-store.Post().Search(teamId, userId, "#hashtag", true)).Data.(*model.PostList)
+ if len(r6.Order) != 1 && r6.Order[0] != o4.Id {
+ t.Fatal("returned wrong serach result")
+ }
+
+ r7 := (<-store.Post().Search(teamId, userId, "#secret", true)).Data.(*model.PostList)
+ if len(r7.Order) != 1 && r7.Order[0] != o5.Id {
+ t.Fatal("returned wrong serach result")
+ }
+
+ r8 := (<-store.Post().Search(teamId, userId, "@thisshouldmatchnothing", true)).Data.(*model.PostList)
+ if len(r8.Order) != 0 {
+ t.Fatal("returned wrong serach result")
+ }
+}
diff --git a/store/sql_session_store.go b/store/sql_session_store.go
new file mode 100644
index 000000000..dddd023e5
--- /dev/null
+++ b/store/sql_session_store.go
@@ -0,0 +1,176 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ l4g "code.google.com/p/log4go"
+ "github.com/mattermost/platform/model"
+)
+
+type SqlSessionStore struct {
+ *SqlStore
+}
+
+func NewSqlSessionStore(sqlStore *SqlStore) SessionStore {
+ us := &SqlSessionStore{sqlStore}
+
+ for _, db := range sqlStore.GetAllConns() {
+ table := db.AddTableWithName(model.Session{}, "Sessions").SetKeys(false, "Id")
+ table.ColMap("Id").SetMaxSize(26)
+ table.ColMap("AltId").SetMaxSize(26)
+ table.ColMap("UserId").SetMaxSize(26)
+ table.ColMap("TeamId").SetMaxSize(26)
+ table.ColMap("DeviceId").SetMaxSize(128)
+ table.ColMap("Roles").SetMaxSize(64)
+ table.ColMap("Props").SetMaxSize(1000)
+ }
+
+ return us
+}
+
+func (me SqlSessionStore) UpgradeSchemaIfNeeded() {
+}
+
+func (me SqlSessionStore) CreateIndexesIfNotExists() {
+ me.CreateIndexIfNotExists("idx_user_id", "Sessions", "UserId")
+}
+
+func (me SqlSessionStore) Save(session *model.Session) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if len(session.Id) > 0 {
+ result.Err = model.NewAppError("SqlSessionStore.Save", "Cannot update existing session", "id="+session.Id)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ session.PreSave()
+
+ if cur := <-me.CleanUpExpiredSessions(session.UserId); cur.Err != nil {
+ l4g.Error("Failed to cleanup sessions in Save err=%v", cur.Err)
+ }
+
+ if err := me.GetMaster().Insert(session); err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.Save", "We couldn't save the session", "id="+session.Id+", "+err.Error())
+ } else {
+ result.Data = session
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (me SqlSessionStore) Get(id string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if obj, err := me.GetReplica().Get(model.Session{}, id); err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "id="+id+", "+err.Error())
+ } else if obj == nil {
+ result.Err = model.NewAppError("SqlSessionStore.Get", "We couldn't find the existing session", "id="+id)
+ } else {
+ result.Data = obj.(*model.Session)
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+
+ }()
+
+ return storeChannel
+}
+
+func (me SqlSessionStore) GetSessions(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+
+ if cur := <-me.CleanUpExpiredSessions(userId); cur.Err != nil {
+ l4g.Error("Failed to cleanup sessions in getSessions err=%v", cur.Err)
+ }
+
+ result := StoreResult{}
+
+ var sessions []*model.Session
+
+ if _, err := me.GetReplica().Select(&sessions, "SELECT * FROM Sessions WHERE UserId = ? ORDER BY LastActivityAt DESC", userId); err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.GetSessions", "We encounted an error while finding user sessions", err.Error())
+ } else {
+
+ result.Data = sessions
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (me SqlSessionStore) Remove(sessionIdOrAlt string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE Id = ? Or AltId = ?", sessionIdOrAlt, sessionIdOrAlt)
+ if err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.RemoveSession", "We couldn't remove the session", "id="+sessionIdOrAlt+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (me SqlSessionStore) CleanUpExpiredSessions(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE UserId = ? AND ExpiresAt != 0 AND ? > ExpiresAt", userId, model.GetMillis()); err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.CleanUpExpiredSessions", "We encounted an error while deleting expired user sessions", err.Error())
+ } else {
+ result.Data = userId
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (me SqlSessionStore) UpdateLastActivityAt(sessionId string, time int64) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := me.GetMaster().Exec("UPDATE Sessions SET LastActivityAt = ? WHERE Id = ?", time, sessionId); err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.UpdateLastActivityAt", "We couldn't update the last_activity_at", "sessionId="+sessionId)
+ } else {
+ result.Data = sessionId
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go
new file mode 100644
index 000000000..edb1d4c14
--- /dev/null
+++ b/store/sql_session_store_test.go
@@ -0,0 +1,134 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "testing"
+)
+
+func TestSessionStoreSave(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+
+ if err := (<-store.Session().Save(&s1)).Err; err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSessionGet(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ <-store.Session().Save(&s1)
+
+ s2 := model.Session{}
+ s2.UserId = s1.UserId
+ s2.TeamId = s1.TeamId
+ <-store.Session().Save(&s2)
+
+ s3 := model.Session{}
+ s3.UserId = s1.UserId
+ s3.TeamId = s1.TeamId
+ s3.ExpiresAt = 1
+ <-store.Session().Save(&s3)
+
+ if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil {
+ t.Fatal(rs1.Err)
+ } else {
+ if rs1.Data.(*model.Session).Id != s1.Id {
+ t.Fatal("should match")
+ }
+ }
+
+ if rs2 := (<-store.Session().GetSessions(s1.UserId)); rs2.Err != nil {
+ t.Fatal(rs2.Err)
+ } else {
+ if len(rs2.Data.([]*model.Session)) != 2 {
+ t.Fatal("should match len")
+ }
+ }
+
+}
+
+func TestSessionRemove(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ <-store.Session().Save(&s1)
+
+ if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil {
+ t.Fatal(rs1.Err)
+ } else {
+ if rs1.Data.(*model.Session).Id != s1.Id {
+ t.Fatal("should match")
+ }
+ }
+
+ <-store.Session().Remove(s1.Id)
+
+ if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil {
+ t.Fatal("should have been removed")
+ }
+}
+
+func TestSessionRemoveAlt(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ <-store.Session().Save(&s1)
+
+ if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil {
+ t.Fatal(rs1.Err)
+ } else {
+ if rs1.Data.(*model.Session).Id != s1.Id {
+ t.Fatal("should match")
+ }
+ }
+
+ <-store.Session().Remove(s1.AltId)
+
+ if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil {
+ t.Fatal("should have been removed")
+ }
+
+ if rs3 := (<-store.Session().GetSessions(s1.UserId)); rs3.Err != nil {
+ t.Fatal(rs3.Err)
+ } else {
+ if len(rs3.Data.([]*model.Session)) != 0 {
+ t.Fatal("should match len")
+ }
+ }
+}
+
+func TestSessionStoreUpdateLastActivityAt(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ <-store.Session().Save(&s1)
+
+ if err := (<-store.Session().UpdateLastActivityAt(s1.Id, 1234567890)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ if r1 := <-store.Session().Get(s1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Session).LastActivityAt != 1234567890 {
+ t.Fatal("LastActivityAt not updated correctly")
+ }
+ }
+
+}
diff --git a/store/sql_store.go b/store/sql_store.go
new file mode 100644
index 000000000..a2deea6ba
--- /dev/null
+++ b/store/sql_store.go
@@ -0,0 +1,372 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ l4g "code.google.com/p/log4go"
+ "crypto/aes"
+ "crypto/cipher"
+ crand "crypto/rand"
+ dbsql "database/sql"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/go-gorp/gorp"
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "io"
+ sqltrace "log"
+ "math/rand"
+ "os"
+ "time"
+)
+
+type SqlStore struct {
+ master *gorp.DbMap
+ replicas []*gorp.DbMap
+ team TeamStore
+ channel ChannelStore
+ post PostStore
+ user UserStore
+ audit AuditStore
+ session SessionStore
+}
+
+func NewSqlStore() Store {
+
+ sqlStore := &SqlStore{}
+
+ sqlStore.master = setupConnection("master", utils.Cfg.SqlSettings.DriverName,
+ utils.Cfg.SqlSettings.DataSource, utils.Cfg.SqlSettings.MaxIdleConns,
+ utils.Cfg.SqlSettings.MaxOpenConns, utils.Cfg.SqlSettings.Trace)
+
+ sqlStore.replicas = make([]*gorp.DbMap, len(utils.Cfg.SqlSettings.DataSourceReplicas))
+ for i, replica := range utils.Cfg.SqlSettings.DataSourceReplicas {
+ sqlStore.replicas[i] = setupConnection(fmt.Sprintf("replica-%v", i), utils.Cfg.SqlSettings.DriverName, replica,
+ utils.Cfg.SqlSettings.MaxIdleConns, utils.Cfg.SqlSettings.MaxOpenConns,
+ utils.Cfg.SqlSettings.Trace)
+ }
+
+ sqlStore.team = NewSqlTeamStore(sqlStore)
+ sqlStore.channel = NewSqlChannelStore(sqlStore)
+ sqlStore.post = NewSqlPostStore(sqlStore)
+ sqlStore.user = NewSqlUserStore(sqlStore)
+ sqlStore.audit = NewSqlAuditStore(sqlStore)
+ sqlStore.session = NewSqlSessionStore(sqlStore)
+
+ sqlStore.master.CreateTablesIfNotExists()
+
+ sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
+ sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
+ sqlStore.post.(*SqlPostStore).CreateIndexesIfNotExists()
+ sqlStore.user.(*SqlUserStore).CreateIndexesIfNotExists()
+ sqlStore.audit.(*SqlAuditStore).CreateIndexesIfNotExists()
+ sqlStore.session.(*SqlSessionStore).CreateIndexesIfNotExists()
+
+ sqlStore.team.(*SqlTeamStore).UpgradeSchemaIfNeeded()
+ sqlStore.channel.(*SqlChannelStore).UpgradeSchemaIfNeeded()
+ sqlStore.post.(*SqlPostStore).UpgradeSchemaIfNeeded()
+ sqlStore.user.(*SqlUserStore).UpgradeSchemaIfNeeded()
+ sqlStore.audit.(*SqlAuditStore).UpgradeSchemaIfNeeded()
+ sqlStore.session.(*SqlSessionStore).UpgradeSchemaIfNeeded()
+
+ return sqlStore
+}
+
+func setupConnection(con_type string, driver string, dataSource string, maxIdle int, maxOpen int, trace bool) *gorp.DbMap {
+
+ db, err := dbsql.Open(driver, dataSource)
+ if err != nil {
+ l4g.Critical("Failed to open sql connection to '%v' err:%v", dataSource, err)
+ time.Sleep(time.Second)
+ panic("Failed to open sql connection" + err.Error())
+ }
+
+ l4g.Info("Pinging sql %v database at '%v'", con_type, dataSource)
+ err = db.Ping()
+ if err != nil {
+ l4g.Critical("Failed to ping db err:%v", err)
+ time.Sleep(time.Second)
+ panic("Failed to open sql connection " + err.Error())
+ }
+
+ db.SetMaxIdleConns(maxIdle)
+ db.SetMaxOpenConns(maxOpen)
+
+ var dbmap *gorp.DbMap
+
+ if driver == "sqlite3" {
+ dbmap = &gorp.DbMap{Db: db, TypeConverter: mattermConverter{}, Dialect: gorp.SqliteDialect{}}
+ } else if driver == "mysql" {
+ dbmap = &gorp.DbMap{Db: db, TypeConverter: mattermConverter{}, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8"}}
+ } else {
+ l4g.Critical("Failed to create dialect specific driver")
+ time.Sleep(time.Second)
+ panic("Failed to create dialect specific driver " + err.Error())
+ }
+
+ if trace {
+ dbmap.TraceOn("", sqltrace.New(os.Stdout, "sql-trace:", sqltrace.Lmicroseconds))
+ }
+
+ return dbmap
+}
+
+func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string, afterName string, colType string, defaultValue string) bool {
+ count, err := ss.GetMaster().SelectInt(
+ `SELECT
+ COUNT(0) AS column_exists
+ FROM
+ information_schema.COLUMNS
+ WHERE
+ TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = ?
+ AND COLUMN_NAME = ?`,
+ tableName,
+ columnName,
+ )
+ if err != nil {
+ l4g.Critical("Failed to check if column exists %v", err)
+ time.Sleep(time.Second)
+ panic("Failed to check if column exists " + err.Error())
+ }
+
+ if count > 0 {
+ return false
+ }
+
+ _, err = ss.GetMaster().Exec("ALTER TABLE " + tableName + " ADD " + columnName + " " + colType + " DEFAULT '" + defaultValue + "'" + " AFTER " + afterName)
+ if err != nil {
+ l4g.Critical("Failed to create column %v", err)
+ time.Sleep(time.Second)
+ panic("Failed to create column " + err.Error())
+ }
+
+ return true
+}
+
+func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) bool {
+ count, err := ss.GetMaster().SelectInt(
+ `SELECT
+ COUNT(0) AS column_exists
+ FROM
+ information_schema.COLUMNS
+ WHERE
+ TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = ?
+ AND COLUMN_NAME = ?`,
+ tableName,
+ columnName,
+ )
+ if err != nil {
+ l4g.Critical("Failed to check if column exists %v", err)
+ time.Sleep(time.Second)
+ panic("Failed to check if column exists " + err.Error())
+ }
+
+ if count == 0 {
+ return false
+ }
+
+ _, err = ss.GetMaster().Exec("ALTER TABLE " + tableName + " DROP COLUMN " + columnName)
+ if err != nil {
+ l4g.Critical("Failed to drop column %v", err)
+ time.Sleep(time.Second)
+ panic("Failed to drop column " + err.Error())
+ }
+
+ return true
+}
+
+func (ss SqlStore) CreateIndexIfNotExists(indexName string, tableName string, columnName string) {
+ ss.createIndexIfNotExists(indexName, tableName, columnName, false)
+}
+
+func (ss SqlStore) CreateFullTextIndexIfNotExists(indexName string, tableName string, columnName string) {
+ ss.createIndexIfNotExists(indexName, tableName, columnName, true)
+}
+
+func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, columnName string, fullText bool) {
+ count, err := ss.GetMaster().SelectInt("SELECT COUNT(0) AS index_exists FROM information_schema.statistics WHERE TABLE_SCHEMA = DATABASE() and table_name = ? AND index_name = ?", tableName, indexName)
+ if err != nil {
+ l4g.Critical("Failed to check index", err)
+ time.Sleep(time.Second)
+ panic("Failed to check index" + err.Error())
+ }
+
+ if count > 0 {
+ return
+ }
+
+ fullTextIndex := ""
+ if fullText {
+ fullTextIndex = " FULLTEXT "
+ }
+
+ _, err = ss.GetMaster().Exec("CREATE " + fullTextIndex + " INDEX " + indexName + " ON " + tableName + " (" + columnName + ")")
+ if err != nil {
+ l4g.Critical("Failed to create index", err)
+ time.Sleep(time.Second)
+ panic("Failed to create index " + err.Error())
+ }
+}
+
+func (ss SqlStore) GetMaster() *gorp.DbMap {
+ return ss.master
+}
+
+func (ss SqlStore) GetReplica() *gorp.DbMap {
+ return ss.replicas[rand.Intn(len(ss.replicas))]
+}
+
+func (ss SqlStore) GetAllConns() []*gorp.DbMap {
+ all := make([]*gorp.DbMap, len(ss.replicas)+1)
+ copy(all, ss.replicas)
+ all[len(ss.replicas)] = ss.master
+ return all
+}
+
+func (ss SqlStore) Close() {
+ l4g.Info("Closing SqlStore")
+ ss.master.Db.Close()
+ for _, replica := range ss.replicas {
+ replica.Db.Close()
+ }
+}
+
+func (ss SqlStore) Team() TeamStore {
+ return ss.team
+}
+
+func (ss SqlStore) Channel() ChannelStore {
+ return ss.channel
+}
+
+func (ss SqlStore) Post() PostStore {
+ return ss.post
+}
+
+func (ss SqlStore) User() UserStore {
+ return ss.user
+}
+
+func (ss SqlStore) Session() SessionStore {
+ return ss.session
+}
+
+func (ss SqlStore) Audit() AuditStore {
+ return ss.audit
+}
+
+type mattermConverter struct{}
+
+func (me mattermConverter) ToDb(val interface{}) (interface{}, error) {
+
+ switch t := val.(type) {
+ case model.StringMap:
+ return model.MapToJson(t), nil
+ case model.StringArray:
+ return model.ArrayToJson(t), nil
+ case model.EncryptStringMap:
+ return encrypt([]byte(utils.Cfg.SqlSettings.AtRestEncryptKey), model.MapToJson(t))
+ }
+
+ return val, nil
+}
+
+func (me mattermConverter) FromDb(target interface{}) (gorp.CustomScanner, bool) {
+ switch target.(type) {
+ case *model.StringMap:
+ binder := func(holder, target interface{}) error {
+ s, ok := holder.(*string)
+ if !ok {
+ return errors.New("FromDb: Unable to convert StringMap to *string")
+ }
+ b := []byte(*s)
+ return json.Unmarshal(b, target)
+ }
+ return gorp.CustomScanner{new(string), target, binder}, true
+ case *model.StringArray:
+ binder := func(holder, target interface{}) error {
+ s, ok := holder.(*string)
+ if !ok {
+ return errors.New("FromDb: Unable to convert StringArray to *string")
+ }
+ b := []byte(*s)
+ return json.Unmarshal(b, target)
+ }
+ return gorp.CustomScanner{new(string), target, binder}, true
+ case *model.EncryptStringMap:
+ binder := func(holder, target interface{}) error {
+ s, ok := holder.(*string)
+ if !ok {
+ return errors.New("FromDb: Unable to convert EncryptStringMap to *string")
+ }
+
+ ue, err := decrypt([]byte(utils.Cfg.SqlSettings.AtRestEncryptKey), *s)
+ if err != nil {
+ return err
+ }
+
+ b := []byte(ue)
+ return json.Unmarshal(b, target)
+ }
+ return gorp.CustomScanner{new(string), target, binder}, true
+ }
+
+ return gorp.CustomScanner{}, false
+}
+
+func encrypt(key []byte, text string) (string, error) {
+
+ if text == "" || text == "{}" {
+ return "", nil
+ }
+
+ plaintext := []byte(text)
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return "", err
+ }
+
+ ciphertext := make([]byte, aes.BlockSize+len(plaintext))
+ iv := ciphertext[:aes.BlockSize]
+ if _, err := io.ReadFull(crand.Reader, iv); err != nil {
+ return "", err
+ }
+
+ stream := cipher.NewCFBEncrypter(block, iv)
+ stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
+
+ return base64.URLEncoding.EncodeToString(ciphertext), nil
+}
+
+func decrypt(key []byte, cryptoText string) (string, error) {
+
+ if cryptoText == "" || cryptoText == "{}" {
+ return "{}", nil
+ }
+
+ ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return "", err
+ }
+
+ if len(ciphertext) < aes.BlockSize {
+ return "", errors.New("ciphertext too short")
+ }
+ iv := ciphertext[:aes.BlockSize]
+ ciphertext = ciphertext[aes.BlockSize:]
+
+ stream := cipher.NewCFBDecrypter(block, iv)
+
+ stream.XORKeyStream(ciphertext, ciphertext)
+
+ return fmt.Sprintf("%s", ciphertext), nil
+}
diff --git a/store/sql_store_test.go b/store/sql_store_test.go
new file mode 100644
index 000000000..84dbf5705
--- /dev/null
+++ b/store/sql_store_test.go
@@ -0,0 +1,83 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "strings"
+ "testing"
+)
+
+var store Store
+
+func Setup() {
+ if store == nil {
+ utils.LoadConfig("config.json")
+ store = NewSqlStore()
+ }
+}
+
+func TestSqlStore1(t *testing.T) {
+ utils.LoadConfig("config.json")
+ utils.Cfg.SqlSettings.Trace = true
+
+ store := NewSqlStore()
+ store.Close()
+
+ utils.LoadConfig("config.json")
+}
+
+func TestSqlStore2(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil {
+ t.Fatal("should have been fatal")
+ }
+ }()
+
+ utils.LoadConfig("config.json")
+ utils.Cfg.SqlSettings.DriverName = "missing"
+ store = NewSqlStore()
+
+ utils.LoadConfig("config.json")
+}
+
+func TestSqlStore3(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil {
+ t.Fatal("should have been fatal")
+ }
+ }()
+
+ utils.LoadConfig("config.json")
+ utils.Cfg.SqlSettings.DataSource = "missing"
+ store = NewSqlStore()
+
+ utils.LoadConfig("config.json")
+}
+
+func TestEncrypt(t *testing.T) {
+ m := make(map[string]string)
+
+ key := []byte("IPc17oYK9NAj6WfJeCqm5AxIBF6WBNuN") // AES-256
+
+ originalText1 := model.MapToJson(m)
+ cryptoText1, _ := encrypt(key, originalText1)
+ text1, _ := decrypt(key, cryptoText1)
+ rm1 := model.MapFromJson(strings.NewReader(text1))
+
+ if len(rm1) != 0 {
+ t.Fatal("error in encrypt")
+ }
+
+ m["key"] = "value"
+ originalText2 := model.MapToJson(m)
+ cryptoText2, _ := encrypt(key, originalText2)
+ text2, _ := decrypt(key, cryptoText2)
+ rm2 := model.MapFromJson(strings.NewReader(text2))
+
+ if rm2["key"] != "value" {
+ t.Fatal("error in encrypt")
+ }
+}
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
new file mode 100644
index 000000000..6e7fc1c1e
--- /dev/null
+++ b/store/sql_team_store.go
@@ -0,0 +1,195 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "strings"
+)
+
+type SqlTeamStore struct {
+ *SqlStore
+}
+
+func NewSqlTeamStore(sqlStore *SqlStore) TeamStore {
+ s := &SqlTeamStore{sqlStore}
+
+ for _, db := range sqlStore.GetAllConns() {
+ table := db.AddTableWithName(model.Team{}, "Teams").SetKeys(false, "Id")
+ table.ColMap("Id").SetMaxSize(26)
+ table.ColMap("Name").SetMaxSize(64)
+ table.ColMap("Domain").SetMaxSize(64).SetUnique(true)
+ table.ColMap("Email").SetMaxSize(128)
+ table.ColMap("CompanyName").SetMaxSize(64)
+ table.ColMap("AllowedDomains").SetMaxSize(500)
+ }
+
+ return s
+}
+
+func (s SqlTeamStore) UpgradeSchemaIfNeeded() {
+}
+
+func (s SqlTeamStore) CreateIndexesIfNotExists() {
+}
+
+func (s SqlTeamStore) Save(team *model.Team) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if len(team.Id) > 0 {
+ result.Err = model.NewAppError("SqlTeamStore.Save",
+ "Must call update for exisiting team", "id="+team.Id)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ team.PreSave()
+ if result.Err = team.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if err := s.GetMaster().Insert(team); err != nil {
+ if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Domain'") {
+ result.Err = model.NewAppError("SqlTeamStore.Save", "A team with that domain already exists", "id="+team.Id+", "+err.Error())
+ } else {
+ result.Err = model.NewAppError("SqlTeamStore.Save", "We couldn't save the team", "id="+team.Id+", "+err.Error())
+ }
+ } else {
+ result.Data = team
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlTeamStore) Update(team *model.Team) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ team.PreUpdate()
+
+ if result.Err = team.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if oldResult, err := s.GetMaster().Get(model.Team{}, team.Id); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.Update", "We encounted an error finding the team", "id="+team.Id+", "+err.Error())
+ } else if oldResult == nil {
+ result.Err = model.NewAppError("SqlTeamStore.Update", "We couldn't find the existing team to update", "id="+team.Id)
+ } else {
+ oldTeam := oldResult.(*model.Team)
+ team.CreateAt = oldTeam.CreateAt
+ team.Domain = oldTeam.Domain
+
+ if count, err := s.GetMaster().Update(team); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.Update", "We encounted an error updating the team", "id="+team.Id+", "+err.Error())
+ } else if count != 1 {
+ result.Err = model.NewAppError("SqlTeamStore.Update", "We couldn't update the team", "id="+team.Id)
+ } else {
+ result.Data = team
+ }
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlTeamStore) UpdateName(name string, teamId string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := s.GetMaster().Exec("UPDATE Teams SET Name = ? WHERE Id = ?", name, teamId); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.UpdateName", "We couldn't update the team name", "team_id="+teamId)
+ } else {
+ result.Data = teamId
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlTeamStore) Get(id string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if obj, err := s.GetReplica().Get(model.Team{}, id); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.Get", "We encounted an error finding the team", "id="+id+", "+err.Error())
+ } else if obj == nil {
+ result.Err = model.NewAppError("SqlTeamStore.Get", "We couldn't find the existing team", "id="+id)
+ } else {
+ result.Data = obj.(*model.Team)
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlTeamStore) GetByDomain(domain string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ team := model.Team{}
+
+ if err := s.GetReplica().SelectOne(&team, "SELECT * FROM Teams WHERE Domain=?", domain); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.GetByDomain", "We couldn't find the existing team", "domain="+domain+", "+err.Error())
+ }
+
+ result.Data = &team
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var data []*model.Team
+ if _, err := s.GetReplica().Select(&data, "SELECT Teams.* FROM Teams, Users WHERE Teams.Id = Users.TeamId AND Users.Email = ?", email); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.GetTeamsForEmail", "We encounted a problem when looking up teams", "email="+email+", "+err.Error())
+ }
+
+ result.Data = data
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_team_store_test.go b/store/sql_team_store_test.go
new file mode 100644
index 000000000..981817d90
--- /dev/null
+++ b/store/sql_team_store_test.go
@@ -0,0 +1,155 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "testing"
+)
+
+func TestTeamStoreSave(t *testing.T) {
+ Setup()
+
+ o1 := model.Team{}
+ o1.Name = "Name"
+ o1.Domain = "a" + model.NewId() + "b"
+ o1.Email = model.NewId() + "@nowhere.com"
+ o1.Type = model.TEAM_OPEN
+
+ if err := (<-store.Team().Save(&o1)).Err; err != nil {
+ t.Fatal("couldn't save item", err)
+ }
+
+ if err := (<-store.Team().Save(&o1)).Err; err == nil {
+ t.Fatal("shouldn't be able to update from save")
+ }
+
+ o1.Id = ""
+ if err := (<-store.Team().Save(&o1)).Err; err == nil {
+ t.Fatal("should be unique domain")
+ }
+}
+
+func TestTeamStoreUpdate(t *testing.T) {
+ Setup()
+
+ o1 := model.Team{}
+ o1.Name = "Name"
+ o1.Domain = "a" + model.NewId() + "b"
+ o1.Email = model.NewId() + "@nowhere.com"
+ o1.Type = model.TEAM_OPEN
+ <-store.Team().Save(&o1)
+
+ if err := (<-store.Team().Update(&o1)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ o1.Id = "missing"
+ if err := (<-store.Team().Update(&o1)).Err; err == nil {
+ t.Fatal("Update should have failed because of missing key")
+ }
+
+ o1.Id = model.NewId()
+ if err := (<-store.Team().Update(&o1)).Err; err == nil {
+ t.Fatal("Update should have faile because id change")
+ }
+}
+
+func TestTeamStoreUpdateName(t *testing.T) {
+ Setup()
+
+ o1 := &model.Team{}
+ o1.Name = "Name"
+ o1.Domain = "a" + model.NewId() + "b"
+ o1.Email = model.NewId() + "@nowhere.com"
+ o1.Type = model.TEAM_OPEN
+ o1 = (<-store.Team().Save(o1)).Data.(*model.Team)
+
+ newName := "NewName"
+
+ if err := (<-store.Team().UpdateName(newName, o1.Id)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ ro1 := (<-store.Team().Get(o1.Id)).Data.(*model.Team)
+ if ro1.Name != newName {
+ t.Fatal("Name not updated")
+ }
+}
+
+func TestTeamStoreGet(t *testing.T) {
+ Setup()
+
+ o1 := model.Team{}
+ o1.Name = "Name"
+ o1.Domain = "a" + model.NewId() + "b"
+ o1.Email = model.NewId() + "@nowhere.com"
+ o1.Type = model.TEAM_OPEN
+ <-store.Team().Save(&o1)
+
+ if r1 := <-store.Team().Get(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Team).ToJson() != o1.ToJson() {
+ t.Fatal("invalid returned team")
+ }
+ }
+
+ if err := (<-store.Team().Get("")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestTeamStoreGetByDomain(t *testing.T) {
+ Setup()
+
+ o1 := model.Team{}
+ o1.Name = "Name"
+ o1.Domain = "a" + model.NewId() + "b"
+ o1.Email = model.NewId() + "@nowhere.com"
+ o1.Type = model.TEAM_OPEN
+ <-store.Team().Save(&o1)
+
+ if r1 := <-store.Team().GetByDomain(o1.Domain); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Team).ToJson() != o1.ToJson() {
+ t.Fatal("invalid returned team")
+ }
+ }
+
+ if err := (<-store.Team().GetByDomain("")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestTeamStoreGetForEmail(t *testing.T) {
+ Setup()
+
+ o1 := model.Team{}
+ o1.Name = "Name"
+ o1.Domain = "a" + model.NewId() + "b"
+ o1.Email = model.NewId() + "@nowhere.com"
+ o1.Type = model.TEAM_OPEN
+ <-store.Team().Save(&o1)
+
+ u1 := model.User{}
+ u1.TeamId = o1.Id
+ u1.Email = model.NewId()
+ <-store.User().Save(&u1)
+
+ if r1 := <-store.Team().GetTeamsForEmail(u1.Email); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ teams := r1.Data.([]*model.Team)
+
+ if teams[0].Id != o1.Id {
+ t.Fatal("failed to lookup by email")
+ }
+ }
+
+ if r1 := <-store.Team().GetTeamsForEmail("missing"); r1.Err != nil {
+ t.Fatal(r1.Err)
+ }
+}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
new file mode 100644
index 000000000..abb8f2781
--- /dev/null
+++ b/store/sql_user_store.go
@@ -0,0 +1,367 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "fmt"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "strings"
+)
+
+type SqlUserStore struct {
+ *SqlStore
+}
+
+func NewSqlUserStore(sqlStore *SqlStore) UserStore {
+ us := &SqlUserStore{sqlStore}
+
+ for _, db := range sqlStore.GetAllConns() {
+ table := db.AddTableWithName(model.User{}, "Users").SetKeys(false, "Id")
+ table.ColMap("Id").SetMaxSize(26)
+ table.ColMap("TeamId").SetMaxSize(26)
+ table.ColMap("Username").SetMaxSize(64)
+ table.ColMap("Password").SetMaxSize(128)
+ table.ColMap("AuthData").SetMaxSize(128)
+ table.ColMap("Email").SetMaxSize(128)
+ table.ColMap("FullName").SetMaxSize(64)
+ table.ColMap("Roles").SetMaxSize(64)
+ table.ColMap("Props").SetMaxSize(4000)
+ table.ColMap("NotifyProps").SetMaxSize(2000)
+ table.SetUniqueTogether("Email", "TeamId")
+ table.SetUniqueTogether("Username", "TeamId")
+ }
+
+ return us
+}
+
+func (s SqlUserStore) UpgradeSchemaIfNeeded() {
+}
+
+func (us SqlUserStore) CreateIndexesIfNotExists() {
+ us.CreateIndexIfNotExists("idx_team_id", "Users", "TeamId")
+}
+
+func (us SqlUserStore) Save(user *model.User) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if len(user.Id) > 0 {
+ result.Err = model.NewAppError("SqlUserStore.Save", "Must call update for exisiting user", "user_id="+user.Id)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ user.PreSave()
+ if result.Err = user.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if count, err := us.GetMaster().SelectInt("SELECT COUNT(0) FROM Users WHERE TeamId = ? AND DeleteAt = 0", user.TeamId); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.Save", "Failed to get current team member count", "teamId="+user.TeamId+", "+err.Error())
+ storeChannel <- result
+ close(storeChannel)
+ return
+ } else if int(count) > utils.Cfg.TeamSettings.MaxUsersPerTeam {
+ result.Err = model.NewAppError("SqlUserStore.Save", "You've reached the limit of the number of allowed accounts.", "teamId="+user.TeamId)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if err := us.GetMaster().Insert(user); err != nil {
+ if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Email'") {
+ result.Err = model.NewAppError("SqlUserStore.Save", "An account with that email already exists.", "user_id="+user.Id+", "+err.Error())
+ } else if strings.Contains(err.Error(), "Duplicate entry") && strings.Contains(err.Error(), "for key 'Username'") {
+ result.Err = model.NewAppError("SqlUserStore.Save", "An account with that username already exists.", "user_id="+user.Id+", "+err.Error())
+ } else {
+ result.Err = model.NewAppError("SqlUserStore.Save", "We couldn't save the account.", "user_id="+user.Id+", "+err.Error())
+ }
+ } else {
+ result.Data = user
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) Update(user *model.User, allowRoleActiveUpdate bool) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ user.PreUpdate()
+
+ if result.Err = user.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if oldUserResult, err := us.GetMaster().Get(model.User{}, user.Id); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.Update", "We encounted an error finding the account", "user_id="+user.Id+", "+err.Error())
+ } else if oldUserResult == nil {
+ result.Err = model.NewAppError("SqlUserStore.Update", "We couldn't find the existing account to update", "user_id="+user.Id)
+ } else {
+ oldUser := oldUserResult.(*model.User)
+ user.CreateAt = oldUser.CreateAt
+ user.AuthData = oldUser.AuthData
+ user.Password = oldUser.Password
+ user.LastPasswordUpdate = oldUser.LastPasswordUpdate
+ user.TeamId = oldUser.TeamId
+ user.LastActivityAt = oldUser.LastActivityAt
+ user.LastPingAt = oldUser.LastPingAt
+ user.EmailVerified = oldUser.EmailVerified
+
+ if !allowRoleActiveUpdate {
+ user.Roles = oldUser.Roles
+ user.DeleteAt = oldUser.DeleteAt
+ }
+
+ if user.Email != oldUser.Email {
+ user.EmailVerified = false
+ }
+
+ if count, err := us.GetMaster().Update(user); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.Update", "We encounted an error updating the account", "user_id="+user.Id+", "+err.Error())
+ } else if count != 1 {
+ result.Err = model.NewAppError("SqlUserStore.Update", "We couldn't update the account", "user_id="+user.Id)
+ } else {
+ result.Data = [2]*model.User{user, oldUser}
+ }
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) UpdateLastPingAt(userId string, time int64) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := us.GetMaster().Exec("UPDATE Users SET LastPingAt = ? WHERE Id = ?", time, userId); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.UpdateLastPingAt", "We couldn't update the last_ping_at", "user_id="+userId)
+ } else {
+ result.Data = userId
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) UpdateLastActivityAt(userId string, time int64) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := us.GetMaster().Exec("UPDATE Users SET LastActivityAt = ? WHERE Id = ?", time, userId); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.UpdateLastActivityAt", "We couldn't update the last_activity_at", "user_id="+userId)
+ } else {
+ result.Data = userId
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := us.GetMaster().Exec("UPDATE Sessions, Users SET Users.LastActivityAt = ?, Sessions.LastActivityAt = ? WHERE Users.Id = ? AND Sessions.Id = ?", time, time, userId, sessionId); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.UpdateLastActivityAt", "We couldn't update the last_activity_at", "user_id="+userId+" session_id="+sessionId+" err="+err.Error())
+ } else {
+ result.Data = userId
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ updateAt := model.GetMillis()
+
+ if _, err := us.GetMaster().Exec("UPDATE Users SET Password = ?, LastPasswordUpdate = ?, UpdateAt = ? WHERE Id = ?", hashedPassword, updateAt, updateAt, userId); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.UpdatePassword", "We couldn't update the user password", "id="+userId+", "+err.Error())
+ } else {
+ result.Data = userId
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) Get(id string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if obj, err := us.GetReplica().Get(model.User{}, id); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.Get", "We encounted an error finding the account", "user_id="+id+", "+err.Error())
+ } else if obj == nil {
+ result.Err = model.NewAppError("SqlUserStore.Get", "We couldn't find the existing account", "user_id="+id)
+ } else {
+ result.Data = obj.(*model.User)
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+
+ }()
+
+ return storeChannel
+}
+
+func (s SqlUserStore) GetEtagForProfiles(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ updateAt, err := s.GetReplica().SelectInt("SELECT UpdateAt FROM Users WHERE TeamId = ? ORDER BY UpdateAt DESC LIMIT 1", teamId)
+ if err != nil {
+ result.Data = fmt.Sprintf("%v.%v", model.ETAG_ROOT_VERSION, model.GetMillis())
+ } else {
+ result.Data = fmt.Sprintf("%v.%v", model.ETAG_ROOT_VERSION, updateAt)
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var users []*model.User
+
+ if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE TeamId = ?", teamId); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetProfiles", "We encounted an error while finding user profiles", err.Error())
+ } else {
+
+ userMap := make(map[string]*model.User)
+
+ for _, u := range users {
+ u.Password = ""
+ u.AuthData = ""
+ userMap[u.Id] = u
+ }
+
+ result.Data = userMap
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) GetByEmail(teamId string, email string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ user := model.User{}
+
+ if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE TeamId=? AND Email=?", teamId, email); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetByEmail", "We couldn't find the existing account", "teamId="+teamId+", email="+email+", "+err.Error())
+ }
+
+ result.Data = &user
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) GetByUsername(teamId string, username string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ user := model.User{}
+
+ if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE TeamId=? AND Username=?", teamId, username); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetByUsername", "We couldn't find the existing account", "teamId="+teamId+", username="+username+", "+err.Error())
+ }
+
+ result.Data = &user
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (us SqlUserStore) VerifyEmail(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := us.GetMaster().Exec("UPDATE Users SET EmailVerified = 1 WHERE Id = ?", userId); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.VerifyEmail", "Unable to update verify email field", "userId="+userId+", "+err.Error())
+ }
+
+ result.Data = userId
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go
new file mode 100644
index 000000000..4231920a5
--- /dev/null
+++ b/store/sql_user_store_test.go
@@ -0,0 +1,276 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "strings"
+ "testing"
+)
+
+func TestUserStoreSave(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.Email = model.NewId()
+ u1.Username = model.NewId()
+ u1.TeamId = model.NewId()
+
+ if err := (<-store.User().Save(&u1)).Err; err != nil {
+ t.Fatal("couldn't save user", err)
+ }
+
+ if err := (<-store.User().Save(&u1)).Err; err == nil {
+ t.Fatal("shouldn't be able to update user from save")
+ }
+
+ u1.Id = ""
+ if err := (<-store.User().Save(&u1)).Err; err == nil {
+ t.Fatal("should be unique email")
+ }
+
+ u1.Email = ""
+ if err := (<-store.User().Save(&u1)).Err; err == nil {
+ t.Fatal("should be unique username")
+ }
+
+ u1.Email = strings.Repeat("0123456789", 20)
+ u1.Username = ""
+ if err := (<-store.User().Save(&u1)).Err; err == nil {
+ t.Fatal("should be unique username")
+ }
+
+ for i := 0; i < 150; i++ {
+ u1.Id = ""
+ u1.Email = model.NewId()
+ u1.Username = model.NewId()
+ if err := (<-store.User().Save(&u1)).Err; err != nil {
+ t.Fatal("couldn't save item", err)
+ }
+ }
+
+ u1.Id = ""
+ u1.Email = model.NewId()
+ u1.Username = model.NewId()
+ if err := (<-store.User().Save(&u1)).Err; err == nil {
+ t.Fatal("should be the limit", err)
+ }
+}
+
+func TestUserStoreUpdate(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ <-store.User().Save(&u1)
+
+ if err := (<-store.User().Update(&u1, false)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ u1.Id = "missing"
+ if err := (<-store.User().Update(&u1, false)).Err; err == nil {
+ t.Fatal("Update should have failed because of missing key")
+ }
+
+ u1.Id = model.NewId()
+ if err := (<-store.User().Update(&u1, false)).Err; err == nil {
+ t.Fatal("Update should have faile because id change")
+ }
+}
+
+func TestUserStoreUpdateLastPingAt(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ <-store.User().Save(&u1)
+
+ if err := (<-store.User().UpdateLastPingAt(u1.Id, 1234567890)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.User).LastPingAt != 1234567890 {
+ t.Fatal("LastPingAt not updated correctly")
+ }
+ }
+
+}
+
+func TestUserStoreUpdateLastActivityAt(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ <-store.User().Save(&u1)
+
+ if err := (<-store.User().UpdateLastActivityAt(u1.Id, 1234567890)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.User).LastActivityAt != 1234567890 {
+ t.Fatal("LastActivityAt not updated correctly")
+ }
+ }
+
+}
+
+func TestUserStoreUpdateUserAndSessionActivity(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ <-store.User().Save(&u1)
+
+ s1 := model.Session{}
+ s1.UserId = u1.Id
+ s1.TeamId = u1.TeamId
+ <-store.Session().Save(&s1)
+
+ if err := (<-store.User().UpdateUserAndSessionActivity(u1.Id, s1.Id, 1234567890)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.User).LastActivityAt != 1234567890 {
+ t.Fatal("LastActivityAt not updated correctly for user")
+ }
+ }
+
+ if r2 := <-store.Session().Get(s1.Id); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ if r2.Data.(*model.Session).LastActivityAt != 1234567890 {
+ t.Fatal("LastActivityAt not updated correctly for session")
+ }
+ }
+
+}
+
+func TestUserStoreGet(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ <-store.User().Save(&u1)
+
+ if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.User).ToJson() != u1.ToJson() {
+ t.Fatal("invalid returned user")
+ }
+ }
+
+ if err := (<-store.User().Get("")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestUserStoreGetProfiles(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ <-store.User().Save(&u1)
+
+ u2 := model.User{}
+ u2.TeamId = u1.TeamId
+ u2.Email = model.NewId()
+ <-store.User().Save(&u2)
+
+ if r1 := <-store.User().GetProfiles(u1.TeamId); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ users := r1.Data.(map[string]*model.User)
+ if len(users) != 2 {
+ t.Fatal("invalid returned users")
+ }
+
+ if users[u1.Id].Id != u1.Id {
+ t.Fatal("invalid returned user")
+ }
+ }
+
+ if r2 := <-store.User().GetProfiles("123"); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ if len(r2.Data.(map[string]*model.User)) != 0 {
+ t.Fatal("should have returned empty map")
+ }
+ }
+}
+
+func TestUserStoreGetByEmail(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ <-store.User().Save(&u1)
+
+ if err := (<-store.User().GetByEmail(u1.TeamId, u1.Email)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ if err := (<-store.User().GetByEmail("", "")).Err; err == nil {
+ t.Fatal("Should have failed because of missing email")
+ }
+}
+
+func TestUserStoreGetByUsername(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ u1.Username = model.NewId()
+ <-store.User().Save(&u1)
+
+ if err := (<-store.User().GetByUsername(u1.TeamId, u1.Username)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ if err := (<-store.User().GetByUsername("", "")).Err; err == nil {
+ t.Fatal("Should have failed because of missing username")
+ }
+}
+
+func TestUserStoreUpdatePassword(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ <-store.User().Save(&u1)
+
+ hashedPassword := model.HashPassword("newpwd")
+
+ if err := (<-store.User().UpdatePassword(u1.Id, hashedPassword)).Err; err != nil {
+ t.Fatal(err)
+ }
+
+ if r1 := <-store.User().GetByEmail(u1.TeamId, u1.Email); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ user := r1.Data.(*model.User)
+ if user.Password != hashedPassword {
+ t.Fatal("Password was not updated correctly")
+ }
+ }
+}
diff --git a/store/store.go b/store/store.go
new file mode 100644
index 000000000..8d4b49b6e
--- /dev/null
+++ b/store/store.go
@@ -0,0 +1,94 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+type StoreResult struct {
+ Data interface{}
+ Err *model.AppError
+}
+
+type StoreChannel chan StoreResult
+
+type Store interface {
+ Team() TeamStore
+ Channel() ChannelStore
+ Post() PostStore
+ User() UserStore
+ Audit() AuditStore
+ Session() SessionStore
+ Close()
+}
+
+type TeamStore interface {
+ Save(team *model.Team) StoreChannel
+ Update(team *model.Team) StoreChannel
+ UpdateName(name string, teamId string) StoreChannel
+ Get(id string) StoreChannel
+ GetByDomain(domain string) StoreChannel
+ GetTeamsForEmail(domain string) StoreChannel
+}
+
+type ChannelStore interface {
+ Save(channel *model.Channel) StoreChannel
+ Update(channel *model.Channel) StoreChannel
+ Get(id string) StoreChannel
+ Delete(channelId string, time int64) StoreChannel
+ GetByName(team_id string, domain string) StoreChannel
+ GetChannels(teamId string, userId string) StoreChannel
+ GetMoreChannels(teamId string, userId string) StoreChannel
+
+ SaveMember(member *model.ChannelMember) StoreChannel
+ GetMembers(channelId string) StoreChannel
+ GetMember(channelId string, userId string) StoreChannel
+ RemoveMember(channelId string, userId string) StoreChannel
+ GetExtraMembers(channelId string, limit int) StoreChannel
+ CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel
+ CheckOpenChannelPermissions(teamId string, channelId string) StoreChannel
+ CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
+ UpdateLastViewedAt(channelId string, userId string) StoreChannel
+ IncrementMentionCount(channelId string, userId string) StoreChannel
+ UpdateNotifyLevel(channelId string, userId string, notifyLevel string) StoreChannel
+}
+
+type PostStore interface {
+ Save(post *model.Post) StoreChannel
+ Update(post *model.Post, newMessage string, newHashtags string) StoreChannel
+ Get(id string) StoreChannel
+ Delete(postId string, time int64) StoreChannel
+ GetPosts(channelId string, offset int, limit int) StoreChannel
+ GetEtag(channelId string) StoreChannel
+ Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel
+}
+
+type UserStore interface {
+ Save(user *model.User) StoreChannel
+ Update(user *model.User, allowRoleUpdate bool) StoreChannel
+ UpdateLastPingAt(userId string, time int64) StoreChannel
+ UpdateLastActivityAt(userId string, time int64) StoreChannel
+ UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel
+ UpdatePassword(userId, newPassword string) StoreChannel
+ Get(id string) StoreChannel
+ GetProfiles(teamId string) StoreChannel
+ GetByEmail(teamId string, email string) StoreChannel
+ GetByUsername(teamId string, username string) StoreChannel
+ VerifyEmail(userId string) StoreChannel
+ GetEtagForProfiles(teamId string) StoreChannel
+}
+
+type SessionStore interface {
+ Save(session *model.Session) StoreChannel
+ Get(id string) StoreChannel
+ GetSessions(userId string) StoreChannel
+ Remove(sessionIdOrAlt string) StoreChannel
+ UpdateLastActivityAt(sessionId string, time int64) StoreChannel
+}
+
+type AuditStore interface {
+ Save(audit *model.Audit) StoreChannel
+ Get(user_id string, limit int) StoreChannel
+}