summaryrefslogtreecommitdiffstats
path: root/model
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 /model
downloadchat-56e74239d6b34df8f30ef046f0b0ff4ff0866a71.tar.gz
chat-56e74239d6b34df8f30ef046f0b0ff4ff0866a71.tar.bz2
chat-56e74239d6b34df8f30ef046f0b0ff4ff0866a71.zip
first commit
Diffstat (limited to 'model')
-rw-r--r--model/audit.go39
-rw-r--r--model/audit_test.go19
-rw-r--r--model/audits.go39
-rw-r--r--model/audits_test.go29
-rw-r--r--model/channel.go104
-rw-r--r--model/channel_extra.go51
-rw-r--r--model/channel_list.go71
-rw-r--r--model/channel_member.go75
-rw-r--r--model/channel_member_test.go60
-rw-r--r--model/channel_test.go81
-rw-r--r--model/client.go652
-rw-r--r--model/command.go53
-rw-r--r--model/command_test.go25
-rw-r--r--model/file.go42
-rw-r--r--model/message.go54
-rw-r--r--model/message_test.go24
-rw-r--r--model/post.go147
-rw-r--r--model/post_list.go91
-rw-r--r--model/post_list_test.go36
-rw-r--r--model/post_test.go87
-rw-r--r--model/session.go119
-rw-r--r--model/session_test.go35
-rw-r--r--model/suggest_command.go34
-rw-r--r--model/suggest_command_test.go19
-rw-r--r--model/team.go141
-rw-r--r--model/team_signup.go40
-rw-r--r--model/team_signup_test.go20
-rw-r--r--model/team_test.go76
-rw-r--r--model/user.go293
-rw-r--r--model/user_test.go92
-rw-r--r--model/utils.go320
-rw-r--r--model/utils_test.go118
32 files changed, 3086 insertions, 0 deletions
diff --git a/model/audit.go b/model/audit.go
new file mode 100644
index 000000000..9f3640350
--- /dev/null
+++ b/model/audit.go
@@ -0,0 +1,39 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type Audit struct {
+ Id string `json:"id"`
+ CreateAt int64 `json:"create_at"`
+ UserId string `json:"user_id"`
+ Action string `json:"action"`
+ ExtraInfo string `json:"extra_info"`
+ IpAddress string `json:"ip_address"`
+ SessionId string `json:"session_id"`
+}
+
+func (o *Audit) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func AuditFromJson(data io.Reader) *Audit {
+ decoder := json.NewDecoder(data)
+ var o Audit
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/audit_test.go b/model/audit_test.go
new file mode 100644
index 000000000..a309f5c65
--- /dev/null
+++ b/model/audit_test.go
@@ -0,0 +1,19 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestAuditJson(t *testing.T) {
+ audit := Audit{Id: NewId(), UserId: NewId(), CreateAt: GetMillis()}
+ json := audit.ToJson()
+ result := AuditFromJson(strings.NewReader(json))
+
+ if audit.Id != result.Id {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/model/audits.go b/model/audits.go
new file mode 100644
index 000000000..9c88deef9
--- /dev/null
+++ b/model/audits.go
@@ -0,0 +1,39 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type Audits []Audit
+
+func (o Audits) Etag() string {
+ if len(o) > 0 {
+ // the first in the list is always the most current
+ return Etag(o[0].CreateAt)
+ } else {
+ return ""
+ }
+}
+
+func (o Audits) ToJson() string {
+ if b, err := json.Marshal(o); err != nil {
+ return "[]"
+ } else {
+ return string(b)
+ }
+}
+
+func AuditsFromJson(data io.Reader) Audits {
+ decoder := json.NewDecoder(data)
+ var o Audits
+ err := decoder.Decode(&o)
+ if err == nil {
+ return o
+ } else {
+ return nil
+ }
+}
diff --git a/model/audits_test.go b/model/audits_test.go
new file mode 100644
index 000000000..59b510f54
--- /dev/null
+++ b/model/audits_test.go
@@ -0,0 +1,29 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestAuditsJson(t *testing.T) {
+ audit := Audit{Id: NewId(), UserId: NewId(), CreateAt: GetMillis()}
+ json := audit.ToJson()
+ result := AuditFromJson(strings.NewReader(json))
+
+ if audit.Id != result.Id {
+ t.Fatal("Ids do not match")
+ }
+
+ var audits Audits = make([]Audit, 1)
+ audits[0] = audit
+
+ ljson := audits.ToJson()
+ results := AuditsFromJson(strings.NewReader(ljson))
+
+ if audits[0].Id != results[0].Id {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/model/channel.go b/model/channel.go
new file mode 100644
index 000000000..ab36d46a2
--- /dev/null
+++ b/model/channel.go
@@ -0,0 +1,104 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ CHANNEL_OPEN = "O"
+ CHANNEL_PRIVATE = "P"
+ CHANNEL_DIRECT = "D"
+ DEFAULT_CHANNEL = "town-square"
+)
+
+type Channel struct {
+ Id string `json:"id"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ TeamId string `json:"team_id"`
+ Type string `json:"type"`
+ DisplayName string `json:"display_name"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ LastPostAt int64 `json:"last_post_at"`
+ TotalMsgCount int64 `json:"total_msg_count"`
+}
+
+func (o *Channel) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func ChannelFromJson(data io.Reader) *Channel {
+ decoder := json.NewDecoder(data)
+ var o Channel
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func (o *Channel) Etag() string {
+ return Etag(o.Id, o.LastPostAt)
+}
+
+func (o *Channel) IsValid() *AppError {
+
+ if len(o.Id) != 26 {
+ return NewAppError("Channel.IsValid", "Invalid Id", "")
+ }
+
+ if o.CreateAt == 0 {
+ return NewAppError("Channel.IsValid", "Create at must be a valid time", "id="+o.Id)
+ }
+
+ if o.UpdateAt == 0 {
+ return NewAppError("Channel.IsValid", "Update at must be a valid time", "id="+o.Id)
+ }
+
+ if len(o.DisplayName) > 64 {
+ return NewAppError("Channel.IsValid", "Invalid display name", "id="+o.Id)
+ }
+
+ if len(o.Name) > 64 {
+ return NewAppError("Channel.IsValid", "Invalid name", "id="+o.Id)
+ }
+
+ if !IsValidChannelIdentifier(o.Name) {
+ return NewAppError("Channel.IsValid", "Name must be 2 or more lowercase alphanumeric characters", "id="+o.Id)
+ }
+
+ if !(o.Type == CHANNEL_OPEN || o.Type == CHANNEL_PRIVATE || o.Type == CHANNEL_DIRECT) {
+ return NewAppError("Channel.IsValid", "Invalid type", "id="+o.Id)
+ }
+
+ if len(o.Description) > 1024 {
+ return NewAppError("Channel.IsValid", "Invalid description", "id="+o.Id)
+ }
+
+ return nil
+}
+
+func (o *Channel) PreSave() {
+ if o.Id == "" {
+ o.Id = NewId()
+ }
+
+ o.CreateAt = GetMillis()
+ o.UpdateAt = o.CreateAt
+}
+
+func (o *Channel) PreUpdate() {
+ o.UpdateAt = GetMillis()
+}
diff --git a/model/channel_extra.go b/model/channel_extra.go
new file mode 100644
index 000000000..a5c9acf71
--- /dev/null
+++ b/model/channel_extra.go
@@ -0,0 +1,51 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type ExtraMember struct {
+ Id string `json:"id"`
+ FullName string `json:"full_name"`
+ Email string `json:"email"`
+ Roles string `json:"roles"`
+ Username string `json:"username"`
+}
+
+func (o *ExtraMember) Sanitize(options map[string]bool) {
+ if len(options) == 0 || !options["email"] {
+ o.Email = ""
+ }
+ if len(options) == 0 || !options["fullname"] {
+ o.FullName = ""
+ }
+}
+
+type ChannelExtra struct {
+ Id string `json:"id"`
+ Members []ExtraMember `json:"members"`
+}
+
+func (o *ChannelExtra) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func ChannelExtraFromJson(data io.Reader) *ChannelExtra {
+ decoder := json.NewDecoder(data)
+ var o ChannelExtra
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/channel_list.go b/model/channel_list.go
new file mode 100644
index 000000000..088dbea2a
--- /dev/null
+++ b/model/channel_list.go
@@ -0,0 +1,71 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type ChannelList struct {
+ Channels []*Channel `json:"channels"`
+ Members map[string]*ChannelMember `json:"members"`
+}
+
+func (o *ChannelList) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func (o *ChannelList) Etag() string {
+
+ id := "0"
+ var t int64 = 0
+ var delta int64 = 0
+
+ for _, v := range o.Channels {
+ if v.LastPostAt > t {
+ t = v.LastPostAt
+ id = v.Id
+ }
+
+ if v.UpdateAt > t {
+ t = v.UpdateAt
+ id = v.Id
+ }
+
+ member := o.Members[v.Id]
+
+ if member != nil {
+ max := v.LastPostAt
+ if v.UpdateAt > max {
+ max = v.UpdateAt
+ }
+
+ delta += max - member.LastViewedAt
+
+ if member.LastViewedAt > t {
+ t = member.LastViewedAt
+ id = v.Id
+ }
+ }
+ }
+
+ return Etag(id, t, delta, len(o.Channels))
+}
+
+func ChannelListFromJson(data io.Reader) *ChannelList {
+ decoder := json.NewDecoder(data)
+ var o ChannelList
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/channel_member.go b/model/channel_member.go
new file mode 100644
index 000000000..720ac4c42
--- /dev/null
+++ b/model/channel_member.go
@@ -0,0 +1,75 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+ "strings"
+)
+
+const (
+ CHANNEL_ROLE_ADMIN = "admin"
+ CHANNEL_NOTIFY_ALL = "all"
+ CHANNEL_NOTIFY_MENTION = "mention"
+ CHANNEL_NOTIFY_NONE = "none"
+ CHANNEL_NOTIFY_QUIET = "quiet"
+)
+
+type ChannelMember struct {
+ ChannelId string `json:"channel_id"`
+ UserId string `json:"user_id"`
+ Roles string `json:"roles"`
+ LastViewedAt int64 `json:"last_viewed_at"`
+ MsgCount int64 `json:"msg_count"`
+ MentionCount int64 `json:"mention_count"`
+ NotifyLevel string `json:"notify_level"`
+}
+
+func (o *ChannelMember) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func ChannelMemberFromJson(data io.Reader) *ChannelMember {
+ decoder := json.NewDecoder(data)
+ var o ChannelMember
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func (o *ChannelMember) IsValid() *AppError {
+
+ if len(o.ChannelId) != 26 {
+ return NewAppError("ChannelMember.IsValid", "Invalid channel id", "")
+ }
+
+ if len(o.UserId) != 26 {
+ return NewAppError("ChannelMember.IsValid", "Invalid user id", "")
+ }
+
+ for _, role := range strings.Split(o.Roles, " ") {
+ if !(role == "" || role == CHANNEL_ROLE_ADMIN) {
+ return NewAppError("ChannelMember.IsValid", "Invalid role", "role="+role)
+ }
+ }
+
+ if len(o.NotifyLevel) > 20 || !IsChannelNotifyLevelValid(o.NotifyLevel) {
+ return NewAppError("ChannelMember.IsValid", "Invalid notify level", "notify_level="+o.NotifyLevel)
+ }
+
+ return nil
+}
+
+func IsChannelNotifyLevelValid(notifyLevel string) bool {
+ return notifyLevel == CHANNEL_NOTIFY_ALL || notifyLevel == CHANNEL_NOTIFY_MENTION || notifyLevel == CHANNEL_NOTIFY_NONE || notifyLevel == CHANNEL_NOTIFY_QUIET
+}
diff --git a/model/channel_member_test.go b/model/channel_member_test.go
new file mode 100644
index 000000000..3b64ffbf7
--- /dev/null
+++ b/model/channel_member_test.go
@@ -0,0 +1,60 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestChannelMemberJson(t *testing.T) {
+ o := ChannelMember{ChannelId: NewId(), UserId: NewId()}
+ json := o.ToJson()
+ ro := ChannelMemberFromJson(strings.NewReader(json))
+
+ if o.ChannelId != ro.ChannelId {
+ t.Fatal("Ids do not match")
+ }
+}
+
+func TestChannelMemberIsValid(t *testing.T) {
+ o := ChannelMember{}
+
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.ChannelId = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Roles = "missing"
+ o.NotifyLevel = CHANNEL_NOTIFY_ALL
+ o.UserId = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Roles = CHANNEL_ROLE_ADMIN
+ o.NotifyLevel = "junk"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.NotifyLevel = "123456789012345678901"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.NotifyLevel = CHANNEL_NOTIFY_ALL
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+
+ o.Roles = ""
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/model/channel_test.go b/model/channel_test.go
new file mode 100644
index 000000000..21fe71889
--- /dev/null
+++ b/model/channel_test.go
@@ -0,0 +1,81 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestChannelJson(t *testing.T) {
+ o := Channel{Id: NewId(), Name: NewId()}
+ json := o.ToJson()
+ ro := ChannelFromJson(strings.NewReader(json))
+
+ if o.Id != ro.Id {
+ t.Fatal("Ids do not match")
+ }
+}
+
+func TestChannelIsValid(t *testing.T) {
+ o := Channel{}
+
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Id = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CreateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.UpdateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.DisplayName = strings.Repeat("01234567890", 20)
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.DisplayName = "1234"
+ o.Name = "ZZZZZZZ"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Name = "zzzzz"
+
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Type = "U"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Type = "P"
+
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestChannelPreSave(t *testing.T) {
+ o := Channel{Name: "test"}
+ o.PreSave()
+ o.Etag()
+}
+
+func TestChannelPreUpdate(t *testing.T) {
+ o := Channel{Name: "test"}
+ o.PreUpdate()
+}
diff --git a/model/client.go b/model/client.go
new file mode 100644
index 000000000..0448828bb
--- /dev/null
+++ b/model/client.go
@@ -0,0 +1,652 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+const (
+ HEADER_REQUEST_ID = "X-Request-ID"
+ HEADER_VERSION_ID = "X-Version-ID"
+ HEADER_ETAG_SERVER = "ETag"
+ HEADER_ETAG_CLIENT = "If-None-Match"
+ HEADER_FORWARDED = "X-Forwarded-For"
+ HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
+ HEADER_TOKEN = "token"
+ HEADER_AUTH = "Authorization"
+)
+
+type Result struct {
+ RequestId string
+ Etag string
+ Data interface{}
+}
+
+type Client struct {
+ Url string // The location of the server like "http://localhost/api/v1"
+ HttpClient *http.Client // The http client
+ AuthToken string
+}
+
+// NewClient constructs a new client with convienence methods for talking to
+// the server.
+func NewClient(url string) *Client {
+ return &Client{url, &http.Client{}, ""}
+}
+
+func (c *Client) DoPost(url string, data string) (*http.Response, *AppError) {
+ rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data))
+
+ if len(c.AuthToken) > 0 {
+ rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
+ }
+
+ if rp, err := c.HttpClient.Do(rq); err != nil {
+ return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error())
+ } else if rp.StatusCode >= 300 {
+ return nil, AppErrorFromJson(rp.Body)
+ } else {
+ return rp, nil
+ }
+}
+
+func (c *Client) DoGet(url string, data string, etag string) (*http.Response, *AppError) {
+ rq, _ := http.NewRequest("GET", c.Url+url, strings.NewReader(data))
+
+ if len(etag) > 0 {
+ rq.Header.Set(HEADER_ETAG_CLIENT, etag)
+ }
+
+ if len(c.AuthToken) > 0 {
+ rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
+ }
+
+ if rp, err := c.HttpClient.Do(rq); err != nil {
+ return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error())
+ } else if rp.StatusCode == 304 {
+ return rp, nil
+ } else if rp.StatusCode >= 300 {
+ return rp, AppErrorFromJson(rp.Body)
+ } else {
+ return rp, nil
+ }
+}
+
+func getCookie(name string, resp *http.Response) *http.Cookie {
+ for _, cookie := range resp.Cookies() {
+ if cookie.Name == name {
+ return cookie
+ }
+ }
+
+ return nil
+}
+
+func (c *Client) Must(result *Result, err *AppError) *Result {
+ if err != nil {
+ panic(err)
+ }
+
+ return result
+}
+
+func (c *Client) SignupTeam(email string, name string) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["email"] = email
+ m["name"] = name
+ if r, err := c.DoPost("/teams/signup", MapToJson(m)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppError) {
+ if r, err := c.DoPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamSignupFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) CreateTeam(team *Team) (*Result, *AppError) {
+ if r, err := c.DoPost("/teams/create", team.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) FindTeamByDomain(domain string, allServers bool) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["domain"] = domain
+ m["all"] = fmt.Sprintf("%v", allServers)
+ if r, err := c.DoPost("/teams/find_team_by_domain", MapToJson(m)); err != nil {
+ return nil, err
+ } else {
+ val := false
+ if body, _ := ioutil.ReadAll(r.Body); string(body) == "true" {
+ val = true
+ }
+
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), val}, nil
+ }
+}
+
+func (c *Client) FindTeams(email string) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["email"] = email
+ if r, err := c.DoPost("/teams/find_teams", MapToJson(m)); err != nil {
+ return nil, err
+ } else {
+
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["email"] = email
+ if r, err := c.DoPost("/teams/email_teams", MapToJson(m)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) {
+ if r, err := c.DoPost("/teams/invite_members", invites.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), InvitesFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdateTeamName(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/teams/update_name", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) {
+ if r, err := c.DoPost("/users/create", user.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Result, *AppError) {
+ if r, err := c.DoPost("/users/create?d="+data+"&h="+hash, user.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetUser(id string, etag string) (*Result, *AppError) {
+ if r, err := c.DoGet("/users/"+id, "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetMe(etag string) (*Result, *AppError) {
+ if r, err := c.DoGet("/users/me", "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
+ if r, err := c.DoGet("/users/profiles", "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) LoginById(id string, password string) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["id"] = id
+ m["password"] = password
+ return c.login(m)
+}
+
+func (c *Client) LoginByEmail(domain string, email string, password string) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["domain"] = domain
+ m["email"] = email
+ m["password"] = password
+ return c.login(m)
+}
+
+func (c *Client) LoginByEmailWithDevice(domain string, email string, password string, deviceId string) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["domain"] = domain
+ m["email"] = email
+ m["password"] = password
+ m["device_id"] = deviceId
+ return c.login(m)
+}
+
+func (c *Client) login(m map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/users/login", MapToJson(m)); err != nil {
+ return nil, err
+ } else {
+ c.AuthToken = r.Header.Get(HEADER_TOKEN)
+ sessionId := getCookie(SESSION_TOKEN, r)
+
+ if c.AuthToken != sessionId.Value {
+ NewAppError("/users/login", "Authentication tokens didn't match", "")
+ }
+
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) Logout() (*Result, *AppError) {
+ if r, err := c.DoPost("/users/logout", ""); err != nil {
+ return nil, err
+ } else {
+ c.AuthToken = ""
+
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) RevokeSession(sessionAltId string) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["id"] = sessionAltId
+
+ if r, err := c.DoPost("/users/revoke_session", MapToJson(m)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetSessions(id string) (*Result, *AppError) {
+ if r, err := c.DoGet("/users/"+id+"/sessions", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), SessionsFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) Command(channelId string, command string, suggest bool) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["command"] = command
+ m["channelId"] = channelId
+ m["suggest"] = strconv.FormatBool(suggest)
+ if r, err := c.DoPost("/command", MapToJson(m)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), CommandFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) {
+ if r, err := c.DoGet("/users/"+id+"/audits", "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), AuditsFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/create", channel.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/create_direct", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/update", channel.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/update_desc", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/update_notify_level", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetChannels(etag string) (*Result, *AppError) {
+ if r, err := c.DoGet("/channels/", "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) {
+ if r, err := c.DoGet("/channels/more", "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) JoinChannel(id string) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/"+id+"/join", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
+ }
+}
+
+func (c *Client) LeaveChannel(id string) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/"+id+"/leave", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
+ }
+}
+
+func (c *Client) DeleteChannel(id string) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/"+id+"/delete", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
+ }
+}
+
+func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) {
+ data := make(map[string]string)
+ data["user_id"] = user_id
+ if r, err := c.DoPost("/channels/"+id+"/add", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
+ }
+}
+
+func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) {
+ data := make(map[string]string)
+ data["user_id"] = user_id
+ if r, err := c.DoPost("/channels/"+id+"/remove", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
+ }
+}
+
+func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/"+channelId+"/update_last_viewed_at", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), nil}, nil
+ }
+}
+
+func (c *Client) GetChannelExtraInfo(id string) (*Result, *AppError) {
+ if r, err := c.DoGet("/channels/"+id+"/extra_info", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), ChannelExtraFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) CreatePost(post *Post) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/"+post.ChannelId+"/create", post.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) CreateValetPost(post *Post) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/"+post.ChannelId+"/valet_create", post.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdatePost(post *Post) (*Result, *AppError) {
+ if r, err := c.DoPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) (*Result, *AppError) {
+ if r, err := c.DoGet(fmt.Sprintf("/channels/%v/posts/%v/%v", channelId, offset, limit), "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) {
+ if r, err := c.DoGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError) {
+ if r, err := c.DoPost(fmt.Sprintf("/channels/%v/post/%v/delete", channelId, postId), ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) SearchPosts(terms string) (*Result, *AppError) {
+ if r, err := c.DoGet("/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UploadFile(url string, data []byte, contentType string) (*Result, *AppError) {
+ rq, _ := http.NewRequest("POST", c.Url+url, bytes.NewReader(data))
+ rq.Header.Set("Content-Type", contentType)
+
+ if len(c.AuthToken) > 0 {
+ rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
+ }
+
+ if rp, err := c.HttpClient.Do(rq); err != nil {
+ return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error())
+ } else if rp.StatusCode >= 300 {
+ return nil, AppErrorFromJson(rp.Body)
+ } else {
+ return &Result{rp.Header.Get(HEADER_REQUEST_ID),
+ rp.Header.Get(HEADER_ETAG_SERVER), FileUploadResponseFromJson(rp.Body)}, nil
+ }
+}
+
+func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) {
+ var rq *http.Request
+ if isFullUrl {
+ rq, _ = http.NewRequest("GET", url, nil)
+ } else {
+ rq, _ = http.NewRequest("GET", c.Url+url, nil)
+ }
+
+ if len(c.AuthToken) > 0 {
+ rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
+ }
+
+ if rp, err := c.HttpClient.Do(rq); err != nil {
+ return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error())
+ } else if rp.StatusCode >= 300 {
+ return nil, AppErrorFromJson(rp.Body)
+ } else {
+ return &Result{rp.Header.Get(HEADER_REQUEST_ID),
+ rp.Header.Get(HEADER_ETAG_SERVER), rp.Body}, nil
+ }
+}
+
+func (c *Client) GetPublicLink(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/files/get_public_link", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdateUser(user *User) (*Result, *AppError) {
+ if r, err := c.DoPost("/users/update", user.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdateUserRoles(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/users/update_roles", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdateActive(userId string, active bool) (*Result, *AppError) {
+ data := make(map[string]string)
+ data["user_id"] = userId
+ data["active"] = strconv.FormatBool(active)
+ if r, err := c.DoPost("/users/update_active", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdateUserNotify(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/users/update_notify", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string) (*Result, *AppError) {
+ data := make(map[string]string)
+ data["current_password"] = currentPassword
+ data["new_password"] = newPassword
+ data["user_id"] = userId
+
+ if r, err := c.DoPost("/users/newpassword", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/users/send_password_reset", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoPost("/users/reset_password", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) GetStatuses() (*Result, *AppError) {
+ if r, err := c.DoGet("/users/status", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) MockSession(sessionToken string) {
+ c.AuthToken = sessionToken
+}
diff --git a/model/command.go b/model/command.go
new file mode 100644
index 000000000..23573205e
--- /dev/null
+++ b/model/command.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ RESP_EXECUTED = "executed"
+)
+
+type Command struct {
+ Command string `json:"command"`
+ Response string `json:"reponse"`
+ GotoLocation string `json:"goto_location"`
+ ChannelId string `json:"channel_id"`
+ Suggest bool `json:"-"`
+ Suggestions []*SuggestCommand `json:"suggestions"`
+}
+
+func (o *Command) AddSuggestion(suggest *SuggestCommand) {
+
+ if o.Suggest {
+ if o.Suggestions == nil {
+ o.Suggestions = make([]*SuggestCommand, 0, 128)
+ }
+
+ o.Suggestions = append(o.Suggestions, suggest)
+ }
+}
+
+func (o *Command) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func CommandFromJson(data io.Reader) *Command {
+ decoder := json.NewDecoder(data)
+ var o Command
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/command_test.go b/model/command_test.go
new file mode 100644
index 000000000..e3b732c02
--- /dev/null
+++ b/model/command_test.go
@@ -0,0 +1,25 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestCommandJson(t *testing.T) {
+
+ command := &Command{Command: NewId(), Suggest: true}
+ command.AddSuggestion(&SuggestCommand{Suggestion: NewId()})
+ json := command.ToJson()
+ result := CommandFromJson(strings.NewReader(json))
+
+ if command.Command != result.Command {
+ t.Fatal("Ids do not match")
+ }
+
+ if command.Suggestions[0].Suggestion != result.Suggestions[0].Suggestion {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/model/file.go b/model/file.go
new file mode 100644
index 000000000..7f5a3f916
--- /dev/null
+++ b/model/file.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ MAX_FILE_SIZE = 50000000 // 50 MB
+)
+
+var (
+ IMAGE_EXTENSIONS = [4]string{".jpg", ".gif", ".bmp", ".png"}
+ IMAGE_MIME_TYPES = map[string]string{".jpg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff"}
+)
+
+type FileUploadResponse struct {
+ Filenames []string `json:"filenames"`
+}
+
+func FileUploadResponseFromJson(data io.Reader) *FileUploadResponse {
+ decoder := json.NewDecoder(data)
+ var o FileUploadResponse
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func (o *FileUploadResponse) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
diff --git a/model/message.go b/model/message.go
new file mode 100644
index 000000000..47f598af8
--- /dev/null
+++ b/model/message.go
@@ -0,0 +1,54 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ ACTION_TYPING = "typing"
+ ACTION_POSTED = "posted"
+ ACTION_POST_EDITED = "post_edited"
+ ACTION_POST_DELETED = "post_deleted"
+ ACTION_VIEWED = "viewed"
+ ACTION_NEW_USER = "new_user"
+)
+
+type Message struct {
+ TeamId string `json:"team_id"`
+ ChannelId string `json:"channel_id"`
+ UserId string `json:"user_id"`
+ Action string `json:"action"`
+ Props map[string]string `json:"props"`
+}
+
+func (m *Message) Add(key string, value string) {
+ m.Props[key] = value
+}
+
+func NewMessage(teamId string, channekId string, userId string, action string) *Message {
+ return &Message{TeamId: teamId, ChannelId: channekId, UserId: userId, Action: action, Props: make(map[string]string)}
+}
+
+func (o *Message) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func MessageFromJson(data io.Reader) *Message {
+ decoder := json.NewDecoder(data)
+ var o Message
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/message_test.go b/model/message_test.go
new file mode 100644
index 000000000..eb0c75193
--- /dev/null
+++ b/model/message_test.go
@@ -0,0 +1,24 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestMessgaeJson(t *testing.T) {
+ m := NewMessage(NewId(), NewId(), NewId(), ACTION_TYPING)
+ m.Add("RootId", NewId())
+ json := m.ToJson()
+ result := MessageFromJson(strings.NewReader(json))
+
+ if m.TeamId != result.TeamId {
+ t.Fatal("Ids do not match")
+ }
+
+ if m.Props["RootId"] != result.Props["RootId"] {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/model/post.go b/model/post.go
new file mode 100644
index 000000000..d10a9f9fc
--- /dev/null
+++ b/model/post.go
@@ -0,0 +1,147 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ POST_DEFAULT = ""
+)
+
+type Post struct {
+ Id string `json:"id"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ UserId string `json:"user_id"`
+ ChannelId string `json:"channel_id"`
+ RootId string `json:"root_id"`
+ ParentId string `json:"parent_id"`
+ OriginalId string `json:"original_id"`
+ Message string `json:"message"`
+ ImgCount int64 `json:"img_count"`
+ Type string `json:"type"`
+ Props StringMap `json:"props"`
+ Hashtags string `json:"hashtags"`
+ Filenames StringArray `json:"filenames"`
+}
+
+func (o *Post) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func PostFromJson(data io.Reader) *Post {
+ decoder := json.NewDecoder(data)
+ var o Post
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func (o *Post) Etag() string {
+ return Etag(o.Id, o.UpdateAt)
+}
+
+func (o *Post) IsValid() *AppError {
+
+ if len(o.Id) != 26 {
+ return NewAppError("Post.IsValid", "Invalid Id", "")
+ }
+
+ if o.CreateAt == 0 {
+ return NewAppError("Post.IsValid", "Create at must be a valid time", "id="+o.Id)
+ }
+
+ if o.UpdateAt == 0 {
+ return NewAppError("Post.IsValid", "Update at must be a valid time", "id="+o.Id)
+ }
+
+ if len(o.UserId) != 26 {
+ return NewAppError("Post.IsValid", "Invalid user id", "")
+ }
+
+ if len(o.ChannelId) != 26 {
+ return NewAppError("Post.IsValid", "Invalid channel id", "")
+ }
+
+ if !(len(o.RootId) == 26 || len(o.RootId) == 0) {
+ return NewAppError("Post.IsValid", "Invalid root id", "")
+ }
+
+ if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) {
+ return NewAppError("Post.IsValid", "Invalid parent id", "")
+ }
+
+ if len(o.ParentId) == 26 && len(o.RootId) == 0 {
+ return NewAppError("Post.IsValid", "Invalid root id must be set if parent id set", "")
+ }
+
+ if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) {
+ return NewAppError("Post.IsValid", "Invalid original id", "")
+ }
+
+ if len(o.Message) > 4000 {
+ return NewAppError("Post.IsValid", "Invalid message", "id="+o.Id)
+ }
+
+ if len(o.Hashtags) > 1000 {
+ return NewAppError("Post.IsValid", "Invalid hashtags", "id="+o.Id)
+ }
+
+ if !(o.Type == POST_DEFAULT) {
+ return NewAppError("Post.IsValid", "Invalid type", "id="+o.Type)
+ }
+
+ if len(ArrayToJson(o.Filenames)) > 4000 {
+ return NewAppError("Post.IsValid", "Invalid filenames", "id="+o.Id)
+ }
+
+ return nil
+}
+
+func (o *Post) PreSave() {
+ if o.Id == "" {
+ o.Id = NewId()
+ }
+
+ o.OriginalId = ""
+
+ o.CreateAt = GetMillis()
+ o.UpdateAt = o.CreateAt
+
+ if o.Props == nil {
+ o.Props = make(map[string]string)
+ }
+
+ if o.Filenames == nil {
+ o.Filenames = []string{}
+ }
+}
+
+func (o *Post) MakeNonNil() {
+ if o.Props == nil {
+ o.Props = make(map[string]string)
+ }
+ if o.Filenames == nil {
+ o.Filenames = []string{}
+ }
+}
+
+func (o *Post) AddProp(key string, value string) {
+
+ o.MakeNonNil()
+
+ o.Props[key] = value
+}
diff --git a/model/post_list.go b/model/post_list.go
new file mode 100644
index 000000000..88e3a9193
--- /dev/null
+++ b/model/post_list.go
@@ -0,0 +1,91 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type PostList struct {
+ Order []string `json:"order"`
+ Posts map[string]*Post `json:"posts"`
+}
+
+func (o *PostList) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func (o *PostList) MakeNonNil() {
+ if o.Order == nil {
+ o.Order = make([]string, 0)
+ }
+
+ if o.Posts == nil {
+ o.Posts = make(map[string]*Post)
+ }
+
+ for _, v := range o.Posts {
+ v.MakeNonNil()
+ }
+}
+
+func (o *PostList) AddOrder(id string) {
+
+ if o.Order == nil {
+ o.Order = make([]string, 0, 128)
+ }
+
+ o.Order = append(o.Order, id)
+}
+
+func (o *PostList) AddPost(post *Post) {
+
+ if o.Posts == nil {
+ o.Posts = make(map[string]*Post)
+ }
+
+ o.Posts[post.Id] = post
+}
+
+func (o *PostList) Etag() string {
+
+ id := "0"
+ var t int64 = 0
+
+ for _, v := range o.Posts {
+ if v.UpdateAt > t {
+ t = v.UpdateAt
+ id = v.Id
+ }
+ }
+
+ return Etag(id, t)
+}
+
+func (o *PostList) IsChannelId(channelId string) bool {
+ for _, v := range o.Posts {
+ if v.ChannelId != channelId {
+ return false
+ }
+ }
+
+ return true
+}
+
+func PostListFromJson(data io.Reader) *PostList {
+ decoder := json.NewDecoder(data)
+ var o PostList
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/post_list_test.go b/model/post_list_test.go
new file mode 100644
index 000000000..7ce36c399
--- /dev/null
+++ b/model/post_list_test.go
@@ -0,0 +1,36 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestPostListJson(t *testing.T) {
+
+ pl := PostList{}
+ p1 := &Post{Id: NewId(), Message: NewId()}
+ pl.AddPost(p1)
+ p2 := &Post{Id: NewId(), Message: NewId()}
+ pl.AddPost(p2)
+
+ pl.AddOrder(p1.Id)
+ pl.AddOrder(p2.Id)
+
+ json := pl.ToJson()
+ rpl := PostListFromJson(strings.NewReader(json))
+
+ if rpl.Posts[p1.Id].Message != p1.Message {
+ t.Fatal("failed to serialize")
+ }
+
+ if rpl.Posts[p2.Id].Message != p2.Message {
+ t.Fatal("failed to serialize")
+ }
+
+ if rpl.Order[1] != p2.Id {
+ t.Fatal("failed to serialize")
+ }
+}
diff --git a/model/post_test.go b/model/post_test.go
new file mode 100644
index 000000000..38f4b4c98
--- /dev/null
+++ b/model/post_test.go
@@ -0,0 +1,87 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestPostJson(t *testing.T) {
+ o := Post{Id: NewId(), Message: NewId()}
+ json := o.ToJson()
+ ro := PostFromJson(strings.NewReader(json))
+
+ if o.Id != ro.Id {
+ t.Fatal("Ids do not match")
+ }
+}
+
+func TestPostIsValid(t *testing.T) {
+ o := Post{}
+
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Id = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CreateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.UpdateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.UserId = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.ChannelId = NewId()
+ o.RootId = "123"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.RootId = ""
+ o.ParentId = "123"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.ParentId = NewId()
+ o.RootId = ""
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.ParentId = ""
+ o.Message = strings.Repeat("0", 4001)
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Message = strings.Repeat("0", 4000)
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+
+ o.Message = "test"
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPostPreSave(t *testing.T) {
+ o := Post{Message: "test"}
+ o.PreSave()
+ o.Etag()
+}
diff --git a/model/session.go b/model/session.go
new file mode 100644
index 000000000..9fd3b9ec3
--- /dev/null
+++ b/model/session.go
@@ -0,0 +1,119 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ SESSION_TOKEN = "MMSID"
+ SESSION_TIME_WEB_IN_DAYS = 365
+ SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS
+ SESSION_TIME_MOBILE_IN_DAYS = 365
+ SESSION_TIME_MOBILE_IN_SECS = 60 * 60 * 24 * SESSION_TIME_MOBILE_IN_DAYS
+ SESSION_CACHE_IN_SECS = 60 * 10
+ SESSION_CACHE_SIZE = 10000
+ SESSION_PROP_PLATFORM = "platform"
+ SESSION_PROP_OS = "os"
+ SESSION_PROP_BROWSER = "browser"
+)
+
+type Session struct {
+ Id string `json:"id"`
+ AltId string `json:"alt_id"`
+ CreateAt int64 `json:"create_at"`
+ ExpiresAt int64 `json:"expires_at"`
+ LastActivityAt int64 `json:"last_activity_at"`
+ UserId string `json:"user_id"`
+ TeamId string `json:"team_id"`
+ DeviceId string `json:"device_id"`
+ Roles string `json:"roles"`
+ Props StringMap `json:"props"`
+}
+
+func (me *Session) ToJson() string {
+ b, err := json.Marshal(me)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func SessionFromJson(data io.Reader) *Session {
+ decoder := json.NewDecoder(data)
+ var me Session
+ err := decoder.Decode(&me)
+ if err == nil {
+ return &me
+ } else {
+ return nil
+ }
+}
+
+func (me *Session) PreSave() {
+ if me.Id == "" {
+ me.Id = NewId()
+ }
+
+ me.AltId = NewId()
+
+ me.CreateAt = GetMillis()
+ me.LastActivityAt = me.CreateAt
+
+ if me.Props == nil {
+ me.Props = make(map[string]string)
+ }
+}
+
+func (me *Session) Sanitize() {
+ me.Id = ""
+}
+
+func (me *Session) IsExpired() bool {
+
+ if me.ExpiresAt <= 0 {
+ return false
+ }
+
+ if GetMillis() > me.ExpiresAt {
+ return true
+ }
+
+ return false
+}
+
+func (me *Session) SetExpireInDays(days int64) {
+ me.ExpiresAt = GetMillis() + (1000 * 60 * 60 * 24 * days)
+}
+
+func (me *Session) AddProp(key string, value string) {
+
+ if me.Props == nil {
+ me.Props = make(map[string]string)
+ }
+
+ me.Props[key] = value
+}
+
+func SessionsToJson(o []*Session) string {
+ if b, err := json.Marshal(o); err != nil {
+ return "[]"
+ } else {
+ return string(b)
+ }
+}
+
+func SessionsFromJson(data io.Reader) []*Session {
+ decoder := json.NewDecoder(data)
+ var o []*Session
+ err := decoder.Decode(&o)
+ if err == nil {
+ return o
+ } else {
+ return nil
+ }
+}
diff --git a/model/session_test.go b/model/session_test.go
new file mode 100644
index 000000000..4df2a0d76
--- /dev/null
+++ b/model/session_test.go
@@ -0,0 +1,35 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestSessionJson(t *testing.T) {
+ session := Session{}
+ session.PreSave()
+ json := session.ToJson()
+ rsession := SessionFromJson(strings.NewReader(json))
+
+ if rsession.Id != session.Id {
+ t.Fatal("Ids do not match")
+ }
+
+ session.Sanitize()
+
+ if session.IsExpired() {
+ t.Fatal("Shouldn't expire")
+ }
+
+ session.ExpiresAt = GetMillis()
+ time.Sleep(10 * time.Millisecond)
+ if !session.IsExpired() {
+ t.Fatal("Should expire")
+ }
+
+ session.SetExpireInDays(10)
+}
diff --git a/model/suggest_command.go b/model/suggest_command.go
new file mode 100644
index 000000000..0c07ac58e
--- /dev/null
+++ b/model/suggest_command.go
@@ -0,0 +1,34 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type SuggestCommand struct {
+ Suggestion string `json:"suggestion"`
+ Description string `json:"description"`
+}
+
+func (o *SuggestCommand) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func SuggestCommandFromJson(data io.Reader) *SuggestCommand {
+ decoder := json.NewDecoder(data)
+ var o SuggestCommand
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/suggest_command_test.go b/model/suggest_command_test.go
new file mode 100644
index 000000000..a92b676ad
--- /dev/null
+++ b/model/suggest_command_test.go
@@ -0,0 +1,19 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestSuggestCommandJson(t *testing.T) {
+ command := &SuggestCommand{Suggestion: NewId()}
+ json := command.ToJson()
+ result := SuggestCommandFromJson(strings.NewReader(json))
+
+ if command.Suggestion != result.Suggestion {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/model/team.go b/model/team.go
new file mode 100644
index 000000000..a510cde78
--- /dev/null
+++ b/model/team.go
@@ -0,0 +1,141 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ TEAM_OPEN = "O"
+ TEAM_INVITE = "I"
+)
+
+type Team struct {
+ Id string `json:"id"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ Name string `json:"name"`
+ Domain string `json:"domain"`
+ Email string `json:"email"`
+ Type string `json:"type"`
+ CompanyName string `json:"company_name"`
+ AllowedDomains string `json:"allowed_domains"`
+}
+
+type Invites struct {
+ Invites []map[string]string `json:"invites"`
+}
+
+func InvitesFromJson(data io.Reader) *Invites {
+ decoder := json.NewDecoder(data)
+ var o Invites
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func (o *Invites) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func (o *Team) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func TeamFromJson(data io.Reader) *Team {
+ decoder := json.NewDecoder(data)
+ var o Team
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func (o *Team) Etag() string {
+ return Etag(o.Id, o.UpdateAt)
+}
+
+func (o *Team) IsValid() *AppError {
+
+ if len(o.Id) != 26 {
+ return NewAppError("Team.IsValid", "Invalid Id", "")
+ }
+
+ if o.CreateAt == 0 {
+ return NewAppError("Team.IsValid", "Create at must be a valid time", "id="+o.Id)
+ }
+
+ if o.UpdateAt == 0 {
+ return NewAppError("Team.IsValid", "Update at must be a valid time", "id="+o.Id)
+ }
+
+ if len(o.Email) > 128 {
+ return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id)
+ }
+
+ if !IsValidEmail(o.Email) {
+ return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id)
+ }
+
+ if len(o.Name) > 64 {
+ return NewAppError("Team.IsValid", "Invalid name", "id="+o.Id)
+ }
+
+ if len(o.Domain) > 64 {
+ return NewAppError("Team.IsValid", "Invalid domain", "id="+o.Id)
+ }
+
+ if IsReservedDomain(o.Domain) {
+ return NewAppError("Team.IsValid", "This URL is unavailable. Please try another.", "id="+o.Id)
+ }
+
+ if !IsValidDomain(o.Domain) {
+ return NewAppError("Team.IsValid", "Domain must be 4 or more lowercase alphanumeric characters", "id="+o.Id)
+ }
+
+ if !(o.Type == TEAM_OPEN || o.Type == TEAM_INVITE) {
+ return NewAppError("Team.IsValid", "Invalid type", "id="+o.Id)
+ }
+
+ if len(o.CompanyName) > 64 {
+ return NewAppError("Team.IsValid", "Invalid company name", "id="+o.Id)
+ }
+
+ if len(o.AllowedDomains) > 500 {
+ return NewAppError("Team.IsValid", "Invalid allowed domains", "id="+o.Id)
+ }
+
+ return nil
+}
+
+func (o *Team) PreSave() {
+ if o.Id == "" {
+ o.Id = NewId()
+ }
+
+ o.CreateAt = GetMillis()
+ o.UpdateAt = o.CreateAt
+}
+
+func (o *Team) PreUpdate() {
+ o.UpdateAt = GetMillis()
+}
diff --git a/model/team_signup.go b/model/team_signup.go
new file mode 100644
index 000000000..143ba8db1
--- /dev/null
+++ b/model/team_signup.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+)
+
+type TeamSignup struct {
+ Team Team `json:"team"`
+ User User `json:"user"`
+ Invites []string `json:"invites"`
+ Data string `json:"data"`
+ Hash string `json:"hash"`
+}
+
+func TeamSignupFromJson(data io.Reader) *TeamSignup {
+ decoder := json.NewDecoder(data)
+ var o TeamSignup
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ fmt.Println(err)
+
+ return nil
+ }
+}
+
+func (o *TeamSignup) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
diff --git a/model/team_signup_test.go b/model/team_signup_test.go
new file mode 100644
index 000000000..f3f74470b
--- /dev/null
+++ b/model/team_signup_test.go
@@ -0,0 +1,20 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestTeamSignupJson(t *testing.T) {
+ team := Team{Id: NewId(), Name: NewId()}
+ o := TeamSignup{Team: team, Data: "data"}
+ json := o.ToJson()
+ ro := TeamSignupFromJson(strings.NewReader(json))
+
+ if o.Team.Id != ro.Team.Id {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/model/team_test.go b/model/team_test.go
new file mode 100644
index 000000000..6261ed6bf
--- /dev/null
+++ b/model/team_test.go
@@ -0,0 +1,76 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestTeamJson(t *testing.T) {
+ o := Team{Id: NewId(), Name: NewId()}
+ json := o.ToJson()
+ ro := TeamFromJson(strings.NewReader(json))
+
+ if o.Id != ro.Id {
+ t.Fatal("Ids do not match")
+ }
+}
+
+func TestTeamIsValid(t *testing.T) {
+ o := Team{}
+
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Id = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CreateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.UpdateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Email = strings.Repeat("01234567890", 20)
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Email = "corey@hulen.com"
+ o.Name = strings.Repeat("01234567890", 20)
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Name = "1234"
+ o.Domain = "ZZZZZZZ"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Domain = "zzzzz"
+ o.Type = TEAM_OPEN
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestTeamPreSave(t *testing.T) {
+ o := Team{Name: "test"}
+ o.PreSave()
+ o.Etag()
+}
+
+func TestTeamPreUpdate(t *testing.T) {
+ o := Team{Name: "test"}
+ o.PreUpdate()
+}
diff --git a/model/user.go b/model/user.go
new file mode 100644
index 000000000..794adcad4
--- /dev/null
+++ b/model/user.go
@@ -0,0 +1,293 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "code.google.com/p/go.crypto/bcrypt"
+ "encoding/json"
+ "io"
+ "regexp"
+ "strings"
+)
+
+const (
+ ROLE_ADMIN = "admin"
+ ROLE_SYSTEM_ADMIN = "system_admin"
+ ROLE_SYSTEM_SUPPORT = "system_support"
+ USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
+ USER_OFFLINE_TIMEOUT = 5 * 60 * 1000 // 5 minutes
+ USER_OFFLINE = "offline"
+ USER_AWAY = "away"
+ USER_ONLINE = "online"
+ USER_NOTIFY_ALL = "all"
+ USER_NOTIFY_MENTION = "mention"
+ USER_NOTIFY_NONE = "none"
+ BOT_USERNAME = "valet"
+)
+
+type User struct {
+ Id string `json:"id"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ TeamId string `json:"team_id"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+ AuthData string `json:"auth_data"`
+ Email string `json:"email"`
+ EmailVerified bool `json:"email_verified"`
+ FullName string `json:"full_name"`
+ Roles string `json:"roles"`
+ LastActivityAt int64 `json:"last_activity_at"`
+ LastPingAt int64 `json:"last_ping_at"`
+ AllowMarketing bool `json:"allow_marketing"`
+ Props StringMap `json:"props"`
+ NotifyProps StringMap `json:"notify_props"`
+ LastPasswordUpdate int64 `json:"last_password_update"`
+}
+
+// IsValid validates the user and returns an error if it isn't configured
+// correctly.
+func (u *User) IsValid() *AppError {
+
+ if len(u.Id) != 26 {
+ return NewAppError("User.IsValid", "Invalid user id", "")
+ }
+
+ if u.CreateAt == 0 {
+ return NewAppError("User.IsValid", "Create at must be a valid time", "user_id="+u.Id)
+ }
+
+ if u.UpdateAt == 0 {
+ return NewAppError("User.IsValid", "Update at must be a valid time", "user_id="+u.Id)
+ }
+
+ if len(u.TeamId) != 26 {
+ return NewAppError("User.IsValid", "Invalid team id", "")
+ }
+
+ if len(u.Username) == 0 || len(u.Username) > 64 {
+ return NewAppError("User.IsValid", "Invalid username", "user_id="+u.Id)
+ }
+
+ validChars, _ := regexp.Compile("^[a-z0-9\\.\\-\\_]+$")
+
+ if !validChars.MatchString(u.Username) {
+ return NewAppError("User.IsValid", "Invalid username", "user_id="+u.Id)
+ }
+
+ if len(u.Email) > 128 || len(u.Email) == 0 {
+ return NewAppError("User.IsValid", "Invalid email", "user_id="+u.Id)
+ }
+
+ if len(u.FullName) > 64 {
+ return NewAppError("User.IsValid", "Invalid full name", "user_id="+u.Id)
+ }
+
+ return nil
+}
+
+// PreSave will set the Id and Username if missing. It will also fill
+// in the CreateAt, UpdateAt times. It will also hash the password. It should
+// be run before saving the user to the db.
+func (u *User) PreSave() {
+ if u.Id == "" {
+ u.Id = NewId()
+ }
+
+ if u.Username == "" {
+ u.Username = NewId()
+ }
+
+ u.Username = strings.ToLower(u.Username)
+ u.Email = strings.ToLower(u.Email)
+
+ u.CreateAt = GetMillis()
+ u.UpdateAt = u.CreateAt
+
+ u.LastPasswordUpdate = u.CreateAt
+
+ if u.Props == nil {
+ u.Props = make(map[string]string)
+ }
+
+ if u.NotifyProps == nil || len(u.NotifyProps) == 0 {
+ u.SetDefaultNotifications()
+ }
+
+ if len(u.Password) > 0 {
+ u.Password = HashPassword(u.Password)
+ }
+}
+
+// PreUpdate should be run before updating the user in the db.
+func (u *User) PreUpdate() {
+ u.Username = strings.ToLower(u.Username)
+ u.Email = strings.ToLower(u.Email)
+ u.UpdateAt = GetMillis()
+
+ if u.NotifyProps == nil || len(u.NotifyProps) == 0 {
+ u.SetDefaultNotifications()
+ } else if _, ok := u.NotifyProps["mention_keys"]; ok {
+ // Remove any blank mention keys
+ splitKeys := strings.Split(u.NotifyProps["mention_keys"], ",")
+ goodKeys := []string{}
+ for _, key := range splitKeys {
+ if len(key) > 0 {
+ goodKeys = append(goodKeys, strings.ToLower(key))
+ }
+ }
+ u.NotifyProps["mention_keys"] = strings.Join(goodKeys, ",")
+ }
+}
+
+func (u *User) SetDefaultNotifications() {
+ u.NotifyProps = make(map[string]string)
+ u.NotifyProps["email"] = "true"
+ u.NotifyProps["desktop"] = USER_NOTIFY_ALL
+ u.NotifyProps["desktop_sound"] = "true"
+ u.NotifyProps["mention_keys"] = u.Username
+ u.NotifyProps["first_name"] = "true"
+ splitName := strings.Split(u.FullName, " ")
+ if len(splitName) > 0 && splitName[0] != "" {
+ u.NotifyProps["mention_keys"] += "," + splitName[0]
+ }
+}
+
+// ToJson convert a User to a json string
+func (u *User) ToJson() string {
+ b, err := json.Marshal(u)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+// Generate a valid strong etag so the browser can cache the results
+func (u *User) Etag() string {
+ return Etag(u.Id, u.UpdateAt)
+}
+
+func (u *User) IsOffline() bool {
+ return (GetMillis()-u.LastPingAt) > USER_OFFLINE_TIMEOUT && (GetMillis()-u.LastActivityAt) > USER_OFFLINE_TIMEOUT
+}
+
+func (u *User) IsAway() bool {
+ return (GetMillis() - u.LastActivityAt) > USER_AWAY_TIMEOUT
+}
+
+// Remove any private data from the user object
+func (u *User) Sanitize(options map[string]bool) {
+ u.Password = ""
+ u.AuthData = ""
+
+ if len(options) != 0 && !options["email"] {
+ u.Email = ""
+ }
+ if len(options) != 0 && !options["fullname"] {
+ u.FullName = ""
+ }
+ if len(options) != 0 && !options["skypeid"] {
+ // TODO - fill in when SkypeId is added to user model
+ }
+ if len(options) != 0 && !options["phonenumber"] {
+ // TODO - fill in when PhoneNumber is added to user model
+ }
+ if len(options) != 0 && !options["passwordupadte"] {
+ u.LastPasswordUpdate = 0
+ }
+}
+
+func (u *User) MakeNonNil() {
+ if u.Props == nil {
+ u.Props = make(map[string]string)
+ }
+
+ if u.NotifyProps == nil {
+ u.NotifyProps = make(map[string]string)
+ }
+}
+
+func (u *User) AddProp(key string, value string) {
+ u.MakeNonNil()
+
+ u.Props[key] = value
+}
+
+func (u *User) AddNotifyProp(key string, value string) {
+ u.MakeNonNil()
+
+ u.NotifyProps[key] = value
+}
+
+// UserFromJson will decode the input and return a User
+func UserFromJson(data io.Reader) *User {
+ decoder := json.NewDecoder(data)
+ var user User
+ err := decoder.Decode(&user)
+ if err == nil {
+ return &user
+ } else {
+ return nil
+ }
+}
+
+func UserMapToJson(u map[string]*User) string {
+ b, err := json.Marshal(u)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func UserMapFromJson(data io.Reader) map[string]*User {
+ decoder := json.NewDecoder(data)
+ var users map[string]*User
+ err := decoder.Decode(&users)
+ if err == nil {
+ return users
+ } else {
+ return nil
+ }
+}
+
+// HashPassword generates a hash using the bcrypt.GenerateFromPassword
+func HashPassword(password string) string {
+ hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
+ if err != nil {
+ panic(err)
+ }
+
+ return string(hash)
+}
+
+// ComparePassword compares the hash
+func ComparePassword(hash string, password string) bool {
+
+ if len(password) == 0 {
+ return false
+ }
+
+ err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
+ return err == nil
+}
+
+func IsUsernameValid(username string) bool {
+
+ var restrictedUsernames = []string {
+ BOT_USERNAME,
+ "all",
+ "channel",
+ }
+
+ for _,restrictedUsername := range restrictedUsernames {
+ if username == restrictedUsername {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/model/user_test.go b/model/user_test.go
new file mode 100644
index 000000000..df9ac19c2
--- /dev/null
+++ b/model/user_test.go
@@ -0,0 +1,92 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestPasswordHash(t *testing.T) {
+ hash := HashPassword("Test")
+
+ if !ComparePassword(hash, "Test") {
+ t.Fatal("Passwords don't match")
+ }
+
+ if ComparePassword(hash, "Test2") {
+ t.Fatal("Passwords should not have matched")
+ }
+}
+
+func TestUserJson(t *testing.T) {
+ user := User{Id: NewId(), Username: NewId()}
+ json := user.ToJson()
+ ruser := UserFromJson(strings.NewReader(json))
+
+ if user.Id != ruser.Id {
+ t.Fatal("Ids do not match")
+ }
+}
+
+func TestUserPreSave(t *testing.T) {
+ user := User{Password: "test"}
+ user.PreSave()
+ user.Etag()
+}
+
+func TestUserPreUpdate(t *testing.T) {
+ user := User{Password: "test"}
+ user.PreUpdate()
+}
+
+func TestUserIsValid(t *testing.T) {
+ user := User{}
+
+ if err := user.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ user.Id = NewId()
+ if err := user.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ user.CreateAt = GetMillis()
+ if err := user.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ user.UpdateAt = GetMillis()
+ if err := user.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ user.TeamId = NewId()
+ if err := user.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ user.Username = NewId() + "^hello#"
+ if err := user.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ user.Username = NewId()
+ user.Email = strings.Repeat("01234567890", 20)
+ if err := user.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ user.Email = "test@nowhere.com"
+ user.FullName = strings.Repeat("01234567890", 20)
+ if err := user.IsValid(); err == nil {
+ t.Fatal()
+ }
+
+ user.FullName = ""
+ if err := user.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/model/utils.go b/model/utils.go
new file mode 100644
index 000000000..8b597000a
--- /dev/null
+++ b/model/utils.go
@@ -0,0 +1,320 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "bytes"
+ "code.google.com/p/go-uuid/uuid"
+ "encoding/base32"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/mail"
+ "regexp"
+ "strings"
+ "time"
+)
+
+const (
+ ETAG_ROOT_VERSION = "10"
+)
+
+type StringMap map[string]string
+type StringArray []string
+type EncryptStringMap map[string]string
+
+// AppError is returned for any http response that's not in the 200 range.
+type AppError struct {
+ Message string `json:"message"` // Message to be display to the end user without debugging information
+ DetailedError string `json:"detailed_error"` // Internal error string to help the developer
+ RequestId string `json:"request_id"` // The RequestId that's also set in the header
+ StatusCode int `json:"status_code"` // The http status code
+ Where string `json:"-"` // The function where it happened in the form of Struct.Func
+}
+
+func (er *AppError) Error() string {
+ return er.Where + ": " + er.Message + ", " + er.DetailedError
+}
+
+func (er *AppError) ToJson() string {
+ b, err := json.Marshal(er)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+// AppErrorFromJson will decode the input and return an AppError
+func AppErrorFromJson(data io.Reader) *AppError {
+ decoder := json.NewDecoder(data)
+ var er AppError
+ err := decoder.Decode(&er)
+ if err == nil {
+ return &er
+ } else {
+ return nil
+ }
+}
+
+func NewAppError(where string, message string, details string) *AppError {
+ ap := &AppError{}
+ ap.Message = message
+ ap.Where = where
+ ap.DetailedError = details
+ ap.StatusCode = 500
+ return ap
+}
+
+var encoding = base32.NewEncoding("ybndrfg8ejkmcpqxot1uwisza345h769")
+
+// NewId is a globally unique identifier. It is a [A-Z0-9] string 26
+// characters long. It is a UUID version 4 Guid that is zbased32 encoded
+// with the padding stripped off.
+func NewId() string {
+ var b bytes.Buffer
+ encoder := base32.NewEncoder(encoding, &b)
+ encoder.Write(uuid.NewRandom())
+ encoder.Close()
+ b.Truncate(26) // removes the '==' padding
+ return b.String()
+}
+
+// GetMillis is a convience method to get milliseconds since epoch.
+func GetMillis() int64 {
+ return time.Now().UnixNano() / int64(time.Millisecond)
+}
+
+// MapToJson converts a map to a json string
+func MapToJson(objmap map[string]string) string {
+ if b, err := json.Marshal(objmap); err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+// MapFromJson will decode the key/value pair map
+func MapFromJson(data io.Reader) map[string]string {
+ decoder := json.NewDecoder(data)
+
+ var objmap map[string]string
+ if err := decoder.Decode(&objmap); err != nil {
+ return make(map[string]string)
+ } else {
+ return objmap
+ }
+}
+
+func ArrayToJson(objmap []string) string {
+ if b, err := json.Marshal(objmap); err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func ArrayFromJson(data io.Reader) []string {
+ decoder := json.NewDecoder(data)
+
+ var objmap []string
+ if err := decoder.Decode(&objmap); err != nil {
+ return make([]string, 0)
+ } else {
+ return objmap
+ }
+}
+
+func IsLower(s string) bool {
+ if strings.ToLower(s) == s {
+ return true
+ }
+
+ return false
+}
+
+func IsValidEmail(email string) bool {
+
+ if !IsLower(email) {
+ return false
+ }
+
+ if _, err := mail.ParseAddress(email); err == nil {
+ return true
+ }
+
+ return false
+}
+
+var reservedDomains = []string{
+ "www",
+ "web",
+ "admin",
+ "support",
+ "notify",
+ "test",
+ "demo",
+ "mail",
+ "team",
+ "channel",
+ "internal",
+ "localhost",
+ "stag",
+ "post",
+ "cluster",
+ "api",
+}
+
+func IsReservedDomain(s string) bool {
+ s = strings.ToLower(s)
+
+ for _, value := range reservedDomains {
+ if strings.Index(s, value) == 0 {
+ return true
+ }
+ }
+
+ return false
+}
+
+func IsValidDomain(s string) bool {
+
+ if !IsValidAlphaNum(s) {
+ return false
+ }
+
+ if len(s) <= 3 {
+ return false
+ }
+
+ return true
+}
+
+var wwwStart = regexp.MustCompile(`^www`)
+var betaStart = regexp.MustCompile(`^beta`)
+var ciStart = regexp.MustCompile(`^ci`)
+
+func GetSubDomain(s string) (string, string) {
+ s = strings.Replace(s, "http://", "", 1)
+ s = strings.Replace(s, "https://", "", 1)
+
+ match := wwwStart.MatchString(s)
+ if match {
+ return "", ""
+ }
+
+ match = betaStart.MatchString(s)
+ if match {
+ return "", ""
+ }
+
+ match = ciStart.MatchString(s)
+ if match {
+ return "", ""
+ }
+
+ parts := strings.Split(s, ".")
+
+ if len(parts) != 3 {
+ return "", ""
+ }
+
+ return parts[0], parts[1]
+}
+
+func IsValidChannelIdentifier(s string) bool {
+
+ if !IsValidAlphaNum(s) {
+ return false
+ }
+
+ if len(s) < 2 {
+ return false
+ }
+
+ return true
+}
+
+var validAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`)
+
+func IsValidAlphaNum(s string) bool {
+ match := validAlphaNum.MatchString(s)
+
+ if !match {
+ return false
+ }
+
+ return true
+}
+
+func Etag(parts ...interface{}) string {
+
+ etag := ETAG_ROOT_VERSION
+
+ for _, part := range parts {
+ etag += fmt.Sprintf(".%v", part)
+ }
+
+ return etag
+}
+
+var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_]*[A-Za-z0-9])$`)
+var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`)
+var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`)
+
+func ParseHashtags(text string) (string, string) {
+ words := strings.Split(strings.Replace(text, "\n", " ", -1), " ")
+
+ hashtagString := ""
+ plainString := ""
+ for _, word := range words {
+ word = puncStart.ReplaceAllString(word, "")
+ word = puncEnd.ReplaceAllString(word, "")
+ if validHashtag.MatchString(word) {
+ hashtagString += " " + word
+ } else {
+ plainString += " " + word
+ }
+ }
+
+ if len(hashtagString) > 1000 {
+ hashtagString = hashtagString[:999]
+ lastSpace := strings.LastIndex(hashtagString, " ")
+ if lastSpace > -1 {
+ hashtagString = hashtagString[:lastSpace]
+ } else {
+ hashtagString = ""
+ }
+ }
+
+ return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString)
+}
+
+func IsFileExtImage(ext string) bool {
+ ext = strings.ToLower(ext)
+ for _, imgExt := range IMAGE_EXTENSIONS {
+ if ext == imgExt {
+ return true
+ }
+ }
+ return false
+}
+
+func GetImageMimeType(ext string) string {
+ ext = strings.ToLower(ext)
+ if len(IMAGE_MIME_TYPES[ext]) == 0 {
+ return "image"
+ } else {
+ return IMAGE_MIME_TYPES[ext]
+ }
+}
+
+func ClearMentionTags(post string) string {
+ post = strings.Replace(post, "<mention>", "", -1)
+ post = strings.Replace(post, "</mention>", "", -1)
+ return post
+}
+
+var UrlRegex = regexp.MustCompile(`^((?:[a-z]+:\/\/)?(?:(?:[a-z0-9\-]+\.)+(?:[a-z]{2}|aero|arpa|biz|com|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|local|internal))(:[0-9]{1,5})?(?:\/[a-z0-9_\-\.~]+)*(\/([a-z0-9_\-\.]*)(?:\?[a-z0-9+_~\-\.%=&amp;]*)?)?(?:#[a-zA-Z0-9!$&'()*+.=-_~:@/?]*)?)(?:\s+|$)$`)
+var PartialUrlRegex = regexp.MustCompile(`/api/v1/files/(get|get_image)/([A-Za-z0-9]{26})/([A-Za-z0-9]{26})/(([A-Za-z0-9]+/)?.+\.[A-Za-z0-9]{3,})`)
diff --git a/model/utils_test.go b/model/utils_test.go
new file mode 100644
index 000000000..a9721042d
--- /dev/null
+++ b/model/utils_test.go
@@ -0,0 +1,118 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestNewId(t *testing.T) {
+ for i := 0; i < 1000; i++ {
+ id := NewId()
+ if len(id) > 26 {
+ t.Fatal("ids shouldn't be longer than 26 chars")
+ }
+ }
+}
+
+func TestAppError(t *testing.T) {
+ err := NewAppError("TestAppError", "message", "")
+ json := err.ToJson()
+ rerr := AppErrorFromJson(strings.NewReader(json))
+ if err.Message != rerr.Message {
+ t.Fatal()
+ }
+
+ err.Error()
+}
+
+func TestMapJson(t *testing.T) {
+
+ m := make(map[string]string)
+ m["id"] = "test_id"
+ json := MapToJson(m)
+
+ rm := MapFromJson(strings.NewReader(json))
+
+ if rm["id"] != "test_id" {
+ t.Fatal("map should be valid")
+ }
+
+ rm2 := MapFromJson(strings.NewReader(""))
+ if len(rm2) > 0 {
+ t.Fatal("make should be ivalid")
+ }
+}
+
+func TestValidEmail(t *testing.T) {
+ if !IsValidEmail("corey@hulen.com") {
+ t.Error("email should be valid")
+ }
+
+ if IsValidEmail("@corey@hulen.com") {
+ t.Error("should be invalid")
+ }
+}
+
+func TestValidLower(t *testing.T) {
+ if !IsLower("corey@hulen.com") {
+ t.Error("should be valid")
+ }
+
+ if IsLower("Corey@hulen.com") {
+ t.Error("should be invalid")
+ }
+}
+
+var domains = []struct {
+ value string
+ expected bool
+}{
+ {"spin-punch", true},
+ {"-spin-punch", false},
+ {"spin-punch-", false},
+ {"spin_punch", false},
+ {"a", false},
+ {"aa", false},
+ {"aaa", false},
+ {"aaa-999b", true},
+ {"b00b", true},
+ {"b))b", false},
+ {"test", true},
+}
+
+func TestValidDomain(t *testing.T) {
+ for _, v := range domains {
+ if IsValidDomain(v.value) != v.expected {
+ t.Errorf("expect %v as %v", v.value, v.expected)
+ }
+ }
+}
+
+var tReservedDomains = []struct {
+ value string
+ expected bool
+}{
+ {"test-hello", true},
+ {"test", true},
+ {"admin", true},
+ {"Admin-punch", true},
+ {"spin-punch-admin", false},
+}
+
+func TestReservedDomain(t *testing.T) {
+ for _, v := range tReservedDomains {
+ if IsReservedDomain(v.value) != v.expected {
+ t.Errorf("expect %v as %v", v.value, v.expected)
+ }
+ }
+}
+
+func TestEtag(t *testing.T) {
+ etag := Etag("hello", 24)
+ if len(etag) <= 0 {
+ t.Fatal()
+ }
+}